Skip to main content

subtr_actor/stats/accumulators/
touch.rs

1use super::*;
2
3const TOUCH_KIND_LABEL_VALUES: [&str; 3] = ["control", "medium_hit", "hard_hit"];
4const TOUCH_SURFACE_LABEL_VALUES: [&str; 3] = ["ground", "air", "wall"];
5const TOUCH_DODGE_STATE_LABEL_VALUES: [&str; 2] = ["no_dodge", "dodge"];
6const TOUCH_INTENTION_LABEL_VALUES: [&str; 7] = [
7    "control",
8    "shot",
9    "save",
10    "challenge",
11    "clear",
12    "pass",
13    "neutral",
14];
15const TOUCH_RECEPTION_LABEL_VALUES: [&str; 2] = ["first_touch", "continuation"];
16
17fn touch_kind_label(value: &str) -> StatLabel {
18    match value {
19        "medium_hit" => StatLabel::new("kind", "medium_hit"),
20        "hard_hit" => StatLabel::new("kind", "hard_hit"),
21        _ => StatLabel::new("kind", "control"),
22    }
23}
24
25fn touch_height_band_label(value: &str) -> StatLabel {
26    match value {
27        "low_air" => StatLabel::new("height_band", "low_air"),
28        "high_air" => StatLabel::new("height_band", "high_air"),
29        _ => StatLabel::new("height_band", "ground"),
30    }
31}
32
33fn touch_surface_label(value: &str) -> StatLabel {
34    match value {
35        "air" => StatLabel::new("surface", "air"),
36        "wall" => StatLabel::new("surface", "wall"),
37        _ => StatLabel::new("surface", "ground"),
38    }
39}
40
41fn touch_dodge_state_label(value: &str) -> StatLabel {
42    match value {
43        "dodge" => StatLabel::new("dodge_state", "dodge"),
44        _ => StatLabel::new("dodge_state", "no_dodge"),
45    }
46}
47
48fn touch_intention_label(value: &str) -> StatLabel {
49    match value {
50        "control" => StatLabel::new("intention", "control"),
51        "shot" => StatLabel::new("intention", "shot"),
52        "save" => StatLabel::new("intention", "save"),
53        "challenge" => StatLabel::new("intention", "challenge"),
54        "clear" => StatLabel::new("intention", "clear"),
55        "pass" => StatLabel::new("intention", "pass"),
56        _ => StatLabel::new("intention", "neutral"),
57    }
58}
59
60fn touch_reception_label(first_touch: bool) -> StatLabel {
61    StatLabel::new(
62        "reception",
63        if first_touch {
64            "first_touch"
65        } else {
66            "continuation"
67        },
68    )
69}
70
71#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, ts_rs::TS)]
72#[ts(export)]
73pub struct TouchStats {
74    pub touch_count: u32,
75    pub control_touch_count: u32,
76    pub medium_hit_count: u32,
77    pub hard_hit_count: u32,
78    pub aerial_touch_count: u32,
79    pub high_aerial_touch_count: u32,
80    #[serde(default)]
81    pub wall_touch_count: u32,
82    #[serde(default)]
83    pub first_touch_count: u32,
84    pub is_last_touch: bool,
85    pub last_touch_time: Option<f32>,
86    pub last_touch_frame: Option<usize>,
87    pub time_since_last_touch: Option<f32>,
88    pub frames_since_last_touch: Option<usize>,
89    pub last_ball_speed_change: Option<f32>,
90    pub max_ball_speed_change: f32,
91    pub cumulative_ball_speed_change: f32,
92    #[serde(default)]
93    pub total_ball_travel_distance: f32,
94    #[serde(default)]
95    pub total_ball_advance_distance: f32,
96    #[serde(default)]
97    pub total_ball_retreat_distance: f32,
98    #[serde(default, skip_serializing_if = "LabeledCounts::is_empty")]
99    pub labeled_touch_counts: LabeledCounts,
100    #[serde(default, skip_serializing_if = "LabeledCounts::is_empty")]
101    pub labeled_intention_counts: LabeledCounts,
102    #[serde(default, skip_serializing_if = "LabeledCounts::is_empty")]
103    pub touch_counts_by_role: LabeledCounts,
104    #[serde(default, skip_serializing_if = "LabeledCounts::is_empty")]
105    pub touch_counts_by_play_depth: LabeledCounts,
106}
107
108impl TouchStats {
109    pub fn average_ball_speed_change(&self) -> f32 {
110        if self.touch_count == 0 {
111            0.0
112        } else {
113            self.cumulative_ball_speed_change / self.touch_count as f32
114        }
115    }
116
117    pub fn touch_count_with_labels(&self, labels: &[StatLabel]) -> u32 {
118        self.labeled_touch_counts.count_matching(labels)
119    }
120
121    pub fn dodge_touch_count(&self) -> u32 {
122        self.touch_count_with_labels(&[StatLabel::new("dodge_state", "dodge")])
123    }
124
125    pub fn dodge_hit_count(&self) -> u32 {
126        self.touch_count_with_labels(&[
127            StatLabel::new("dodge_state", "dodge"),
128            StatLabel::new("kind", "medium_hit"),
129        ]) + self.touch_count_with_labels(&[
130            StatLabel::new("dodge_state", "dodge"),
131            StatLabel::new("kind", "hard_hit"),
132        ])
133    }
134
135    pub fn intention_count(&self, intention: &str) -> u32 {
136        self.labeled_intention_counts
137            .count_matching(&[touch_intention_label(intention)])
138    }
139
140    pub fn first_touch_intention_count(&self, intention: &str) -> u32 {
141        self.labeled_intention_counts.count_matching(&[
142            touch_intention_label(intention),
143            touch_reception_label(true),
144        ])
145    }
146
147    pub fn complete_labeled_intention_counts(&self) -> LabeledCounts {
148        let mut entries: Vec<_> = TOUCH_INTENTION_LABEL_VALUES
149            .into_iter()
150            .flat_map(|intention| {
151                TOUCH_RECEPTION_LABEL_VALUES
152                    .into_iter()
153                    .map(move |reception| {
154                        let mut labels = vec![
155                            StatLabel::new("intention", intention),
156                            StatLabel::new("reception", reception),
157                        ];
158                        labels.sort();
159                        LabeledCountEntry {
160                            count: self.labeled_intention_counts.count_exact(&labels),
161                            labels,
162                        }
163                    })
164            })
165            .collect();
166
167        entries.sort_by(|left, right| left.labels.cmp(&right.labels));
168
169        LabeledCounts { entries }
170    }
171
172    pub fn touch_count_with_role(&self, role: RoleState) -> u32 {
173        self.touch_counts_by_role.count_exact(&[role.as_label()])
174    }
175
176    pub fn touch_count_with_play_depth(&self, play_depth: PlayDepthState) -> u32 {
177        self.touch_counts_by_play_depth
178            .count_exact(&[play_depth.as_label()])
179    }
180
181    pub fn touches_as_first_man(&self) -> u32 {
182        self.touch_count_with_role(RoleState::FirstMan)
183    }
184
185    pub fn touches_as_second_man(&self) -> u32 {
186        self.touch_count_with_role(RoleState::SecondMan)
187    }
188
189    pub fn touches_as_third_man(&self) -> u32 {
190        self.touch_count_with_role(RoleState::ThirdMan)
191    }
192
193    pub fn touches_behind_play(&self) -> u32 {
194        self.touch_count_with_play_depth(PlayDepthState::BehindPlay)
195    }
196
197    pub fn touches_ahead_of_play(&self) -> u32 {
198        self.touch_count_with_play_depth(PlayDepthState::AheadOfPlay)
199    }
200
201    pub fn complete_touch_counts_by_role(&self) -> LabeledCounts {
202        let mut entries: Vec<_> = ALL_ROLE_STATES
203            .into_iter()
204            .map(|role| {
205                let labels = vec![role.as_label()];
206                LabeledCountEntry {
207                    count: self.touch_counts_by_role.count_exact(&labels),
208                    labels,
209                }
210            })
211            .collect();
212        entries.sort_by(|left, right| left.labels.cmp(&right.labels));
213        LabeledCounts { entries }
214    }
215
216    pub fn complete_touch_counts_by_play_depth(&self) -> LabeledCounts {
217        let mut entries: Vec<_> = ALL_PLAY_DEPTH_STATES
218            .into_iter()
219            .map(|play_depth| {
220                let labels = vec![play_depth.as_label()];
221                LabeledCountEntry {
222                    count: self.touch_counts_by_play_depth.count_exact(&labels),
223                    labels,
224                }
225            })
226            .collect();
227        entries.sort_by(|left, right| left.labels.cmp(&right.labels));
228        LabeledCounts { entries }
229    }
230
231    pub fn complete_labeled_touch_counts(&self) -> LabeledCounts {
232        let mut entries: Vec<_> = ALL_PLAYER_VERTICAL_BANDS
233            .into_iter()
234            .flat_map(|height_band| {
235                TOUCH_SURFACE_LABEL_VALUES
236                    .into_iter()
237                    .flat_map(move |surface| {
238                        TOUCH_DODGE_STATE_LABEL_VALUES
239                            .into_iter()
240                            .flat_map(move |dodge_state| {
241                                TOUCH_KIND_LABEL_VALUES.into_iter().map(move |kind| {
242                                    let mut labels = vec![
243                                        StatLabel::new("kind", kind),
244                                        height_band.as_label(),
245                                        StatLabel::new("surface", surface),
246                                        StatLabel::new("dodge_state", dodge_state),
247                                    ];
248                                    labels.sort();
249                                    LabeledCountEntry {
250                                        count: self.labeled_touch_counts.count_exact(&labels),
251                                        labels,
252                                    }
253                                })
254                            })
255                    })
256            })
257            .collect();
258
259        entries.sort_by(|left, right| left.labels.cmp(&right.labels));
260
261        LabeledCounts { entries }
262    }
263
264    pub fn with_complete_labeled_touch_counts(mut self) -> Self {
265        self.labeled_touch_counts = self.complete_labeled_touch_counts();
266        self.labeled_intention_counts = self.complete_labeled_intention_counts();
267        self
268    }
269}
270
271#[derive(Debug, Clone, Default, PartialEq)]
272pub struct TouchStatsAccumulator {
273    player_stats: HashMap<PlayerId, TouchStats>,
274    current_last_touch_player: Option<PlayerId>,
275}
276
277impl TouchStatsAccumulator {
278    pub fn new() -> Self {
279        Self::default()
280    }
281
282    pub fn player_stats(&self) -> &HashMap<PlayerId, TouchStats> {
283        &self.player_stats
284    }
285
286    pub fn begin_sample(&mut self, frame: &FrameInfo) {
287        for stats in self.player_stats.values_mut() {
288            stats.is_last_touch = false;
289            stats.time_since_last_touch = stats
290                .last_touch_time
291                .map(|time| (frame.time - time).max(0.0));
292            stats.frames_since_last_touch = stats
293                .last_touch_frame
294                .map(|last_frame| frame.frame_number.saturating_sub(last_frame));
295        }
296    }
297
298    pub fn apply_touch_event(&mut self, event: &TouchClassificationEvent, frame: &FrameInfo) {
299        let stats = self.player_stats.entry(event.player.clone()).or_default();
300        stats.touch_count += 1;
301        match event.height_band.as_str() {
302            "low_air" => stats.aerial_touch_count += 1,
303            "high_air" => {
304                stats.aerial_touch_count += 1;
305                stats.high_aerial_touch_count += 1;
306            }
307            _ => {}
308        }
309        match event.kind.as_str() {
310            "control" => stats.control_touch_count += 1,
311            "medium_hit" => stats.medium_hit_count += 1,
312            "hard_hit" => stats.hard_hit_count += 1,
313            _ => {}
314        }
315        if event.surface == "wall" {
316            stats.wall_touch_count += 1;
317        }
318        stats.labeled_touch_counts.increment([
319            touch_kind_label(&event.kind),
320            touch_height_band_label(&event.height_band),
321            touch_surface_label(&event.surface),
322            touch_dodge_state_label(&event.dodge_state),
323        ]);
324        if event.first_touch {
325            stats.first_touch_count += 1;
326        }
327        stats.labeled_intention_counts.increment([
328            touch_intention_label(&event.intention),
329            touch_reception_label(event.first_touch),
330        ]);
331        stats
332            .touch_counts_by_role
333            .increment([event.role.as_label()]);
334        stats
335            .touch_counts_by_play_depth
336            .increment([event.play_depth.as_label()]);
337        stats.last_touch_time = Some(event.time);
338        stats.last_touch_frame = Some(event.frame);
339        stats.time_since_last_touch = Some((frame.time - event.time).max(0.0));
340        stats.frames_since_last_touch = Some(frame.frame_number.saturating_sub(event.frame));
341        stats.last_ball_speed_change = Some(event.ball_speed_change);
342        stats.max_ball_speed_change = stats.max_ball_speed_change.max(event.ball_speed_change);
343        stats.cumulative_ball_speed_change += event.ball_speed_change;
344        if let Some(movement) = event.ball_movement.as_ref() {
345            stats.total_ball_travel_distance += movement.travel_distance;
346            stats.total_ball_advance_distance += movement.advance_distance;
347            stats.total_ball_retreat_distance += movement.retreat_distance;
348        }
349        self.current_last_touch_player = Some(event.player.clone());
350    }
351
352    pub fn set_current_last_touch_player(&mut self, player: Option<PlayerId>) {
353        self.current_last_touch_player = player;
354    }
355
356    pub fn restore_current_last_touch_marker(&mut self) {
357        if let Some(player_id) = self.current_last_touch_player.as_ref() {
358            if let Some(stats) = self.player_stats.get_mut(player_id) {
359                stats.is_last_touch = true;
360            }
361        }
362    }
363}