Skip to main content

subtr_actor/stats/accumulators/
flick.rs

1use super::*;
2
3const FLICK_HIGH_CONFIDENCE: f32 = 0.80;
4
5/// Per-player accumulated flick stats with confidence.
6#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, ts_rs::TS)]
7#[ts(export)]
8pub struct FlickStats {
9    pub count: u32,
10    pub high_confidence_count: u32,
11    pub is_last_flick: bool,
12    pub last_flick_time: Option<f32>,
13    pub last_flick_frame: Option<usize>,
14    pub time_since_last_flick: Option<f32>,
15    pub frames_since_last_flick: Option<usize>,
16    pub last_confidence: Option<f32>,
17    pub best_confidence: f32,
18    pub cumulative_confidence: f32,
19    pub cumulative_setup_duration: f32,
20    pub cumulative_ball_speed_change: f32,
21    #[serde(default, skip_serializing_if = "LabeledCounts::is_empty")]
22    pub labeled_event_counts: LabeledCounts,
23}
24
25impl FlickStats {
26    pub fn average_confidence(&self) -> f32 {
27        if self.count == 0 {
28            0.0
29        } else {
30            self.cumulative_confidence / self.count as f32
31        }
32    }
33
34    pub fn average_setup_duration(&self) -> f32 {
35        if self.count == 0 {
36            0.0
37        } else {
38            self.cumulative_setup_duration / self.count as f32
39        }
40    }
41
42    pub fn average_ball_speed_change(&self) -> f32 {
43        if self.count == 0 {
44            0.0
45        } else {
46            self.cumulative_ball_speed_change / self.count as f32
47        }
48    }
49
50    fn record_event(&mut self, event: &FlickEvent) {
51        self.labeled_event_counts.increment([
52            confidence_band_label(event.confidence >= FLICK_HIGH_CONFIDENCE),
53            flick_kind_label(&event.kind),
54            flick_setup_rotation_direction_label(&event.setup_rotation_direction),
55        ]);
56        self.sync_legacy_counts();
57        self.last_flick_time = Some(event.time);
58        self.last_flick_frame = Some(event.frame);
59        self.last_confidence = Some(event.confidence);
60        self.best_confidence = self.best_confidence.max(event.confidence);
61        self.cumulative_confidence += event.confidence;
62        self.cumulative_setup_duration += event.setup_duration;
63        self.cumulative_ball_speed_change += event.ball_speed_change;
64    }
65
66    pub fn event_count_with_labels(&self, labels: &[StatLabel]) -> u32 {
67        self.labeled_event_counts.count_matching(labels)
68    }
69
70    pub fn complete_labeled_event_counts(&self) -> LabeledCounts {
71        LabeledCounts::complete_from_label_sets(
72            &[
73                &CONFIDENCE_BAND_LABELS,
74                &FLICK_KIND_LABELS,
75                &FLICK_SETUP_ROTATION_DIRECTION_LABELS,
76            ],
77            &self.labeled_event_counts,
78        )
79    }
80
81    fn sync_legacy_counts(&mut self) {
82        self.count = self.labeled_event_counts.total();
83        self.high_confidence_count = self.event_count_with_labels(&[confidence_band_label(true)]);
84    }
85}
86
87/// Accumulates flick stats over the replay.
88#[derive(Debug, Clone, Default, PartialEq)]
89pub struct FlickStatsAccumulator {
90    player_stats: HashMap<PlayerId, FlickStats>,
91    current_last_flick_player: Option<PlayerId>,
92}
93
94impl FlickStatsAccumulator {
95    pub fn new() -> Self {
96        Self::default()
97    }
98
99    pub fn player_stats(&self) -> &HashMap<PlayerId, FlickStats> {
100        &self.player_stats
101    }
102
103    pub fn begin_sample(&mut self, frame: &FrameInfo) {
104        for stats in self.player_stats.values_mut() {
105            stats.is_last_flick = false;
106            stats.time_since_last_flick = stats
107                .last_flick_time
108                .map(|time| (frame.time - time).max(0.0));
109            stats.frames_since_last_flick = stats
110                .last_flick_frame
111                .map(|last_frame| frame.frame_number.saturating_sub(last_frame));
112        }
113
114        if let Some(player_id) = self.current_last_flick_player.as_ref() {
115            if let Some(stats) = self.player_stats.get_mut(player_id) {
116                stats.is_last_flick = true;
117            }
118        }
119    }
120
121    pub fn apply_event(&mut self, event: &FlickEvent, frame: &FrameInfo) {
122        let stats = self.player_stats.entry(event.player.clone()).or_default();
123        stats.record_event(event);
124        stats.is_last_flick = true;
125        stats.time_since_last_flick = Some((frame.time - event.time).max(0.0));
126        stats.frames_since_last_flick = Some(frame.frame_number.saturating_sub(event.frame));
127
128        self.current_last_flick_player = Some(event.player.clone());
129    }
130
131    pub fn reset_current_last_event_marker(&mut self) {
132        self.current_last_flick_player = None;
133    }
134}