Skip to main content

subtr_actor/stats/calculators/
continuous_ball_control.rs

1use super::*;
2
3#[derive(Debug, Clone, Default, PartialEq)]
4pub struct ContinuousBallControlState {
5    pub completed_sequences: Vec<CompletedBallControlSequence<BallCarryKind>>,
6}
7
8#[derive(Debug, Clone, Copy)]
9pub struct ContinuousBallControlSample<K> {
10    pub kind: K,
11    pub player_position: glam::Vec3,
12    pub horizontal_gap: f32,
13    pub vertical_gap: f32,
14    pub speed: f32,
15}
16
17#[derive(Debug, Clone)]
18pub struct ContinuousBallControlCandidate<K> {
19    pub player_id: PlayerId,
20    pub is_team_0: bool,
21    pub sample: ContinuousBallControlSample<K>,
22}
23
24#[derive(Debug, Clone, PartialEq)]
25pub struct CompletedBallControlSequence<K> {
26    pub player_id: PlayerId,
27    pub is_team_0: bool,
28    pub kind: K,
29    pub start_frame: usize,
30    pub end_frame: usize,
31    pub start_time: f32,
32    pub end_time: f32,
33    pub duration: f32,
34    pub straight_line_distance: f32,
35    pub path_distance: f32,
36    pub average_horizontal_gap: f32,
37    pub average_vertical_gap: f32,
38    pub average_speed: f32,
39}
40
41#[derive(Debug, Clone)]
42struct ActiveBallControlSequence<K> {
43    player_id: PlayerId,
44    is_team_0: bool,
45    kind: K,
46    start_frame: usize,
47    last_frame: usize,
48    start_time: f32,
49    last_time: f32,
50    start_position: glam::Vec3,
51    last_position: glam::Vec3,
52    duration: f32,
53    path_distance: f32,
54    horizontal_gap_integral: f32,
55    vertical_gap_integral: f32,
56    speed_integral: f32,
57}
58
59#[derive(Debug, Clone)]
60pub struct ContinuousBallControlTracker<K> {
61    active_sequence: Option<ActiveBallControlSequence<K>>,
62}
63
64impl<K> Default for ContinuousBallControlTracker<K> {
65    fn default() -> Self {
66        Self {
67            active_sequence: None,
68        }
69    }
70}
71
72impl<K> ContinuousBallControlTracker<K>
73where
74    K: Copy + PartialEq,
75{
76    fn begin_sequence(
77        frame: &FrameInfo,
78        candidate: ContinuousBallControlCandidate<K>,
79    ) -> ActiveBallControlSequence<K> {
80        let sample = candidate.sample;
81        ActiveBallControlSequence {
82            player_id: candidate.player_id,
83            is_team_0: candidate.is_team_0,
84            kind: sample.kind,
85            start_frame: frame.frame_number.saturating_sub(1),
86            last_frame: frame.frame_number,
87            start_time: (frame.time - frame.dt).max(0.0),
88            last_time: frame.time,
89            start_position: sample.player_position,
90            last_position: sample.player_position,
91            duration: frame.dt,
92            path_distance: 0.0,
93            horizontal_gap_integral: sample.horizontal_gap * frame.dt,
94            vertical_gap_integral: sample.vertical_gap * frame.dt,
95            speed_integral: sample.speed * frame.dt,
96        }
97    }
98
99    fn extend_sequence(
100        active_sequence: &mut ActiveBallControlSequence<K>,
101        frame: &FrameInfo,
102        sample: ContinuousBallControlSample<K>,
103    ) {
104        active_sequence.duration += frame.dt;
105        active_sequence.path_distance += sample
106            .player_position
107            .distance(active_sequence.last_position);
108        active_sequence.last_position = sample.player_position;
109        active_sequence.last_time = frame.time;
110        active_sequence.last_frame = frame.frame_number;
111        active_sequence.horizontal_gap_integral += sample.horizontal_gap * frame.dt;
112        active_sequence.vertical_gap_integral += sample.vertical_gap * frame.dt;
113        active_sequence.speed_integral += sample.speed * frame.dt;
114    }
115
116    fn complete_sequence(
117        active_sequence: ActiveBallControlSequence<K>,
118    ) -> CompletedBallControlSequence<K> {
119        CompletedBallControlSequence {
120            player_id: active_sequence.player_id,
121            is_team_0: active_sequence.is_team_0,
122            kind: active_sequence.kind,
123            start_frame: active_sequence.start_frame,
124            end_frame: active_sequence.last_frame,
125            start_time: active_sequence.start_time,
126            end_time: active_sequence.last_time,
127            duration: active_sequence.duration,
128            straight_line_distance: active_sequence
129                .start_position
130                .truncate()
131                .distance(active_sequence.last_position.truncate()),
132            path_distance: active_sequence.path_distance,
133            average_horizontal_gap: active_sequence.horizontal_gap_integral
134                / active_sequence.duration,
135            average_vertical_gap: active_sequence.vertical_gap_integral / active_sequence.duration,
136            average_speed: active_sequence.speed_integral / active_sequence.duration,
137        }
138    }
139
140    fn finish_active_sequence<F>(
141        &mut self,
142        min_duration_for_kind: F,
143    ) -> Option<CompletedBallControlSequence<K>>
144    where
145        F: Fn(K) -> f32,
146    {
147        let active_sequence = self.active_sequence.take()?;
148        if active_sequence.duration < min_duration_for_kind(active_sequence.kind) {
149            return None;
150        }
151        Some(Self::complete_sequence(active_sequence))
152    }
153
154    pub fn update<F>(
155        &mut self,
156        frame: &FrameInfo,
157        candidate: Option<ContinuousBallControlCandidate<K>>,
158        min_duration_for_kind: F,
159    ) -> Vec<CompletedBallControlSequence<K>>
160    where
161        F: Fn(K) -> f32 + Copy,
162    {
163        let mut completed = Vec::new();
164        let Some(candidate) = candidate else {
165            if let Some(sequence) = self.finish_active_sequence(min_duration_for_kind) {
166                completed.push(sequence);
167            }
168            return completed;
169        };
170
171        let same_sequence = self
172            .active_sequence
173            .as_ref()
174            .is_some_and(|active_sequence| {
175                active_sequence.player_id == candidate.player_id
176                    && active_sequence.kind == candidate.sample.kind
177            });
178
179        if same_sequence {
180            if let Some(active_sequence) = self.active_sequence.as_mut() {
181                Self::extend_sequence(active_sequence, frame, candidate.sample);
182            }
183        } else {
184            if let Some(sequence) = self.finish_active_sequence(min_duration_for_kind) {
185                completed.push(sequence);
186            }
187            self.active_sequence = Some(Self::begin_sequence(frame, candidate));
188        }
189
190        completed
191    }
192
193    pub fn finish<F>(&mut self, min_duration_for_kind: F) -> Option<CompletedBallControlSequence<K>>
194    where
195        F: Fn(K) -> f32,
196    {
197        self.finish_active_sequence(min_duration_for_kind)
198    }
199}