Skip to main content

subtr_actor/stats/calculators/
air_dribble.rs

1use super::*;
2
3const AIR_DRIBBLE_MIN_BALL_Z: f32 = 300.0;
4pub(crate) const AIR_DRIBBLE_MIN_PLAYER_Z: f32 = 100.0;
5const AIR_DRIBBLE_MAX_HORIZONTAL_GAP: f32 = BALL_RADIUS_Z * 3.0;
6const AIR_DRIBBLE_MAX_ABOVE_CAR_GAP: f32 = 360.0;
7const AIR_DRIBBLE_MAX_BELOW_CAR_GAP: f32 = 100.0;
8pub(crate) const AIR_DRIBBLE_MIN_DURATION: f32 = 0.65;
9const AIR_DRIBBLE_MIN_TOUCHES: u32 = 3;
10const AIR_DRIBBLE_MIN_AIR_TOUCHES: u32 = 2;
11const WALL_TAKEOFF_MIN_Z: f32 = 120.0;
12const SIDE_WALL_START_ABS_X: f32 = 3200.0;
13const BACK_WALL_START_ABS_Y: f32 = 4600.0;
14
15#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, ts_rs::TS)]
16#[ts(export)]
17pub struct AirDribbleStats {
18    pub count: u32,
19    #[serde(default)]
20    pub ground_to_air_count: u32,
21    #[serde(default)]
22    pub wall_to_air_count: u32,
23    #[serde(default)]
24    pub total_touch_count: u32,
25    #[serde(default)]
26    pub max_touch_count: u32,
27    pub total_time: f32,
28    pub total_straight_line_distance: f32,
29    pub total_path_distance: f32,
30    pub longest_time: f32,
31    pub furthest_distance: f32,
32    pub fastest_speed: f32,
33    pub speed_sum: f32,
34    pub average_horizontal_gap_sum: f32,
35    pub average_vertical_gap_sum: f32,
36}
37
38impl AirDribbleStats {
39    fn count_average(&self, value: f32) -> f32 {
40        if self.count == 0 {
41            0.0
42        } else {
43            value / self.count as f32
44        }
45    }
46
47    pub fn average_time(&self) -> f32 {
48        self.count_average(self.total_time)
49    }
50
51    pub fn average_straight_line_distance(&self) -> f32 {
52        self.count_average(self.total_straight_line_distance)
53    }
54
55    pub fn average_path_distance(&self) -> f32 {
56        self.count_average(self.total_path_distance)
57    }
58
59    pub fn average_speed(&self) -> f32 {
60        self.count_average(self.speed_sum)
61    }
62
63    pub fn average_touch_count(&self) -> f32 {
64        self.count_average(self.total_touch_count as f32)
65    }
66
67    pub fn average_horizontal_gap(&self) -> f32 {
68        self.count_average(self.average_horizontal_gap_sum)
69    }
70
71    pub fn average_vertical_gap(&self) -> f32 {
72        self.count_average(self.average_vertical_gap_sum)
73    }
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS)]
77#[ts(export)]
78#[serde(rename_all = "snake_case")]
79pub enum AirDribbleOrigin {
80    GroundToAir,
81    WallToAir,
82}
83
84impl AirDribbleOrigin {
85    pub fn as_label_value(self) -> &'static str {
86        match self {
87            Self::GroundToAir => "ground_to_air",
88            Self::WallToAir => "wall_to_air",
89        }
90    }
91}
92
93pub(crate) struct AirDribblePolicy;
94
95impl AirDribblePolicy {
96    pub(crate) fn is_sample(
97        player_position: glam::Vec3,
98        ball_position: glam::Vec3,
99        horizontal_gap: f32,
100        vertical_gap: f32,
101    ) -> bool {
102        ball_position.z >= AIR_DRIBBLE_MIN_BALL_Z
103            && player_position.z >= AIR_DRIBBLE_MIN_PLAYER_Z
104            && !player_is_on_wall(player_position)
105            && horizontal_gap <= AIR_DRIBBLE_MAX_HORIZONTAL_GAP
106            && (-AIR_DRIBBLE_MAX_BELOW_CAR_GAP..=AIR_DRIBBLE_MAX_ABOVE_CAR_GAP)
107                .contains(&vertical_gap)
108    }
109
110    pub(crate) fn is_air_touch_position(player_position: glam::Vec3) -> bool {
111        player_position.z > PLAYER_GROUND_Z_THRESHOLD && !player_is_on_wall(player_position)
112    }
113
114    pub(crate) fn kind_requires_airborne(kind: BallCarryKind) -> bool {
115        kind == BallCarryKind::AirDribble
116    }
117
118    pub(crate) fn is_valid_sequence(
119        sequence: &CompletedBallControlSequence<BallCarryKind>,
120    ) -> bool {
121        sequence.kind != BallCarryKind::AirDribble
122            || (sequence.touch_count >= AIR_DRIBBLE_MIN_TOUCHES
123                && sequence.air_touch_count >= AIR_DRIBBLE_MIN_AIR_TOUCHES)
124    }
125
126    pub(crate) fn origin(start_position: glam::Vec3) -> AirDribbleOrigin {
127        if start_position.z >= WALL_TAKEOFF_MIN_Z
128            && (start_position.x.abs() >= SIDE_WALL_START_ABS_X
129                || start_position.y.abs() >= BACK_WALL_START_ABS_Y)
130        {
131            AirDribbleOrigin::WallToAir
132        } else {
133            AirDribbleOrigin::GroundToAir
134        }
135    }
136
137    pub(crate) fn apply_event(stats: &mut AirDribbleStats, event: &BallCarryEvent) {
138        stats.count += 1;
139        stats.total_time += event.duration;
140        stats.total_straight_line_distance += event.straight_line_distance;
141        stats.total_path_distance += event.path_distance;
142        stats.longest_time = stats.longest_time.max(event.duration);
143        stats.furthest_distance = stats.furthest_distance.max(event.straight_line_distance);
144        stats.fastest_speed = stats.fastest_speed.max(event.average_speed);
145        stats.speed_sum += event.average_speed;
146        stats.average_horizontal_gap_sum += event.average_horizontal_gap;
147        stats.average_vertical_gap_sum += event.average_vertical_gap;
148        stats.total_touch_count += event.touch_count;
149        stats.max_touch_count = stats.max_touch_count.max(event.touch_count);
150        match event.air_dribble_origin {
151            Some(AirDribbleOrigin::GroundToAir) => stats.ground_to_air_count += 1,
152            Some(AirDribbleOrigin::WallToAir) => stats.wall_to_air_count += 1,
153            None => {}
154        }
155    }
156}
157
158#[cfg(test)]
159#[path = "air_dribble_tests.rs"]
160mod tests;