Skip to main content

subtr_actor/stats/calculators/
ball_carry.rs

1use super::*;
2
3#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, ts_rs::TS)]
4#[ts(export)]
5pub struct BallCarryStats {
6    pub carry_count: u32,
7    pub total_carry_time: f32,
8    pub total_straight_line_distance: f32,
9    pub total_path_distance: f32,
10    pub longest_carry_time: f32,
11    pub furthest_carry_distance: f32,
12    pub fastest_carry_speed: f32,
13    pub carry_speed_sum: f32,
14    pub average_horizontal_gap_sum: f32,
15    pub average_vertical_gap_sum: f32,
16}
17
18impl BallCarryStats {
19    fn pct_count_average(&self, value: f32) -> f32 {
20        if self.carry_count == 0 {
21            0.0
22        } else {
23            value / self.carry_count as f32
24        }
25    }
26
27    pub fn average_carry_time(&self) -> f32 {
28        self.pct_count_average(self.total_carry_time)
29    }
30
31    pub fn average_straight_line_distance(&self) -> f32 {
32        self.pct_count_average(self.total_straight_line_distance)
33    }
34
35    pub fn average_path_distance(&self) -> f32 {
36        self.pct_count_average(self.total_path_distance)
37    }
38
39    pub fn average_carry_speed(&self) -> f32 {
40        self.pct_count_average(self.carry_speed_sum)
41    }
42
43    pub fn average_horizontal_gap(&self) -> f32 {
44        self.pct_count_average(self.average_horizontal_gap_sum)
45    }
46
47    pub fn average_vertical_gap(&self) -> f32 {
48        self.pct_count_average(self.average_vertical_gap_sum)
49    }
50}
51
52#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, ts_rs::TS)]
53#[ts(export)]
54pub struct AirDribbleStats {
55    pub count: u32,
56    pub total_time: f32,
57    pub total_straight_line_distance: f32,
58    pub total_path_distance: f32,
59    pub longest_time: f32,
60    pub furthest_distance: f32,
61    pub fastest_speed: f32,
62    pub speed_sum: f32,
63    pub average_horizontal_gap_sum: f32,
64    pub average_vertical_gap_sum: f32,
65}
66
67impl AirDribbleStats {
68    fn count_average(&self, value: f32) -> f32 {
69        if self.count == 0 {
70            0.0
71        } else {
72            value / self.count as f32
73        }
74    }
75
76    pub fn average_time(&self) -> f32 {
77        self.count_average(self.total_time)
78    }
79
80    pub fn average_straight_line_distance(&self) -> f32 {
81        self.count_average(self.total_straight_line_distance)
82    }
83
84    pub fn average_path_distance(&self) -> f32 {
85        self.count_average(self.total_path_distance)
86    }
87
88    pub fn average_speed(&self) -> f32 {
89        self.count_average(self.speed_sum)
90    }
91
92    pub fn average_horizontal_gap(&self) -> f32 {
93        self.count_average(self.average_horizontal_gap_sum)
94    }
95
96    pub fn average_vertical_gap(&self) -> f32 {
97        self.count_average(self.average_vertical_gap_sum)
98    }
99}
100
101#[derive(Debug, Clone, PartialEq, Serialize)]
102pub struct BallCarryEvent {
103    pub player_id: PlayerId,
104    pub is_team_0: bool,
105    pub kind: BallCarryKind,
106    pub start_frame: usize,
107    pub end_frame: usize,
108    pub start_time: f32,
109    pub end_time: f32,
110    pub duration: f32,
111    pub straight_line_distance: f32,
112    pub path_distance: f32,
113    pub average_horizontal_gap: f32,
114    pub average_vertical_gap: f32,
115    pub average_speed: f32,
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS)]
119#[ts(export)]
120#[serde(rename_all = "snake_case")]
121pub enum BallCarryKind {
122    Carry,
123    AirDribble,
124}
125
126#[derive(Debug, Clone, Default)]
127pub struct BallCarryCalculator {
128    player_stats: HashMap<PlayerId, BallCarryStats>,
129    player_air_dribble_stats: HashMap<PlayerId, AirDribbleStats>,
130    team_zero_stats: BallCarryStats,
131    team_one_stats: BallCarryStats,
132    team_zero_air_dribble_stats: AirDribbleStats,
133    team_one_air_dribble_stats: AirDribbleStats,
134    carry_events: Vec<BallCarryEvent>,
135    processed_control_sequence_count: usize,
136}
137
138impl BallCarryCalculator {
139    pub fn new() -> Self {
140        Self::default()
141    }
142
143    pub fn player_stats(&self) -> &HashMap<PlayerId, BallCarryStats> {
144        &self.player_stats
145    }
146
147    pub fn player_air_dribble_stats(&self) -> &HashMap<PlayerId, AirDribbleStats> {
148        &self.player_air_dribble_stats
149    }
150
151    pub fn team_zero_stats(&self) -> &BallCarryStats {
152        &self.team_zero_stats
153    }
154
155    pub fn team_one_stats(&self) -> &BallCarryStats {
156        &self.team_one_stats
157    }
158
159    pub fn team_zero_air_dribble_stats(&self) -> &AirDribbleStats {
160        &self.team_zero_air_dribble_stats
161    }
162
163    pub fn team_one_air_dribble_stats(&self) -> &AirDribbleStats {
164        &self.team_one_air_dribble_stats
165    }
166
167    pub fn carry_events(&self) -> &[BallCarryEvent] {
168        &self.carry_events
169    }
170
171    pub(crate) fn carry_frame_sample(
172        player: &PlayerSample,
173        ball: &BallSample,
174    ) -> Option<ContinuousBallControlSample<BallCarryKind>> {
175        let player_position = player.position()?;
176        let ball_position = ball.position();
177        let horizontal_gap = player_position
178            .truncate()
179            .distance(ball_position.truncate());
180        let vertical_gap = ball_position.z - player_position.z;
181
182        if Self::is_air_dribble_sample(player_position, ball_position, horizontal_gap, vertical_gap)
183        {
184            return Some(ContinuousBallControlSample {
185                player_position,
186                kind: BallCarryKind::AirDribble,
187                horizontal_gap,
188                vertical_gap,
189                speed: player.speed().unwrap_or(0.0),
190            });
191        }
192
193        if !(BALL_CARRY_MIN_BALL_Z..=BALL_CARRY_MAX_BALL_Z).contains(&ball_position.z) {
194            return None;
195        }
196
197        if horizontal_gap > BALL_CARRY_MAX_HORIZONTAL_GAP {
198            return None;
199        }
200
201        if !(0.0..=BALL_CARRY_MAX_VERTICAL_GAP).contains(&vertical_gap) {
202            return None;
203        }
204
205        Some(ContinuousBallControlSample {
206            player_position,
207            kind: BallCarryKind::Carry,
208            horizontal_gap,
209            vertical_gap,
210            speed: player.speed().unwrap_or(0.0),
211        })
212    }
213
214    fn is_air_dribble_sample(
215        player_position: glam::Vec3,
216        ball_position: glam::Vec3,
217        horizontal_gap: f32,
218        vertical_gap: f32,
219    ) -> bool {
220        ball_position.z >= AIR_DRIBBLE_MIN_BALL_Z
221            && player_position.z >= AIR_DRIBBLE_MIN_PLAYER_Z
222            && horizontal_gap <= AIR_DRIBBLE_MAX_HORIZONTAL_GAP
223            && (-AIR_DRIBBLE_MAX_BELOW_CAR_GAP..=AIR_DRIBBLE_MAX_ABOVE_CAR_GAP)
224                .contains(&vertical_gap)
225    }
226
227    pub(crate) fn min_duration_for_kind(kind: BallCarryKind) -> f32 {
228        match kind {
229            BallCarryKind::Carry => BALL_CARRY_MIN_DURATION,
230            BallCarryKind::AirDribble => AIR_DRIBBLE_MIN_DURATION,
231        }
232    }
233
234    pub(crate) fn control_candidate(
235        ball: &BallFrameState,
236        players: &PlayerFrameState,
237        live_play: bool,
238        controlling_player: Option<&PlayerId>,
239    ) -> Option<ContinuousBallControlCandidate<BallCarryKind>> {
240        if !live_play {
241            return None;
242        }
243        let ball = ball.sample()?;
244        let player_id = controlling_player?;
245        players
246            .players
247            .iter()
248            .find(|player| &player.player_id == player_id)
249            .and_then(|player| {
250                Self::carry_frame_sample(player, ball).map(|sample| {
251                    ContinuousBallControlCandidate {
252                        player_id: player.player_id.clone(),
253                        is_team_0: player.is_team_0,
254                        sample,
255                    }
256                })
257            })
258    }
259
260    fn event_from_sequence(
261        sequence: CompletedBallControlSequence<BallCarryKind>,
262    ) -> BallCarryEvent {
263        BallCarryEvent {
264            player_id: sequence.player_id,
265            is_team_0: sequence.is_team_0,
266            kind: sequence.kind,
267            start_frame: sequence.start_frame,
268            end_frame: sequence.end_frame,
269            start_time: sequence.start_time,
270            end_time: sequence.end_time,
271            duration: sequence.duration,
272            straight_line_distance: sequence.straight_line_distance,
273            path_distance: sequence.path_distance,
274            average_horizontal_gap: sequence.average_horizontal_gap,
275            average_vertical_gap: sequence.average_vertical_gap,
276            average_speed: sequence.average_speed,
277        }
278    }
279
280    fn record_carry_event(&mut self, event: BallCarryEvent) {
281        match event.kind {
282            BallCarryKind::Carry => {
283                let player_stats = self
284                    .player_stats
285                    .entry(event.player_id.clone())
286                    .or_default();
287                Self::apply_carry_event(player_stats, &event);
288
289                let team_stats = if event.is_team_0 {
290                    &mut self.team_zero_stats
291                } else {
292                    &mut self.team_one_stats
293                };
294                Self::apply_carry_event(team_stats, &event);
295            }
296            BallCarryKind::AirDribble => {
297                let player_stats = self
298                    .player_air_dribble_stats
299                    .entry(event.player_id.clone())
300                    .or_default();
301                Self::apply_air_dribble_event(player_stats, &event);
302
303                let team_stats = if event.is_team_0 {
304                    &mut self.team_zero_air_dribble_stats
305                } else {
306                    &mut self.team_one_air_dribble_stats
307                };
308                Self::apply_air_dribble_event(team_stats, &event);
309            }
310        }
311        self.carry_events.push(event);
312    }
313
314    fn apply_carry_event(stats: &mut BallCarryStats, event: &BallCarryEvent) {
315        stats.carry_count += 1;
316        stats.total_carry_time += event.duration;
317        stats.total_straight_line_distance += event.straight_line_distance;
318        stats.total_path_distance += event.path_distance;
319        stats.longest_carry_time = stats.longest_carry_time.max(event.duration);
320        stats.furthest_carry_distance = stats
321            .furthest_carry_distance
322            .max(event.straight_line_distance);
323        stats.fastest_carry_speed = stats.fastest_carry_speed.max(event.average_speed);
324        stats.carry_speed_sum += event.average_speed;
325        stats.average_horizontal_gap_sum += event.average_horizontal_gap;
326        stats.average_vertical_gap_sum += event.average_vertical_gap;
327    }
328
329    fn apply_air_dribble_event(stats: &mut AirDribbleStats, event: &BallCarryEvent) {
330        stats.count += 1;
331        stats.total_time += event.duration;
332        stats.total_straight_line_distance += event.straight_line_distance;
333        stats.total_path_distance += event.path_distance;
334        stats.longest_time = stats.longest_time.max(event.duration);
335        stats.furthest_distance = stats.furthest_distance.max(event.straight_line_distance);
336        stats.fastest_speed = stats.fastest_speed.max(event.average_speed);
337        stats.speed_sum += event.average_speed;
338        stats.average_horizontal_gap_sum += event.average_horizontal_gap;
339        stats.average_vertical_gap_sum += event.average_vertical_gap;
340    }
341
342    pub fn update(&mut self, control_state: &ContinuousBallControlState) -> SubtrActorResult<()> {
343        for sequence in control_state
344            .completed_sequences
345            .iter()
346            .skip(self.processed_control_sequence_count)
347            .cloned()
348        {
349            self.record_carry_event(Self::event_from_sequence(sequence));
350        }
351        self.processed_control_sequence_count = control_state.completed_sequences.len();
352        Ok(())
353    }
354}
355
356#[cfg(test)]
357#[path = "ball_carry_tests.rs"]
358mod tests;