subtr_actor/stats/accumulators/
touch.rs1use 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}