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