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    #[serde(default, skip_serializing_if = "LabeledCounts::is_empty")]
37    pub labeled_event_counts: LabeledCounts,
38}
39
40impl AirDribbleStats {
41    fn count_average(&self, value: f32) -> f32 {
42        if self.count == 0 {
43            0.0
44        } else {
45            value / self.count as f32
46        }
47    }
48
49    pub fn average_time(&self) -> f32 {
50        self.count_average(self.total_time)
51    }
52
53    pub fn average_straight_line_distance(&self) -> f32 {
54        self.count_average(self.total_straight_line_distance)
55    }
56
57    pub fn average_path_distance(&self) -> f32 {
58        self.count_average(self.total_path_distance)
59    }
60
61    pub fn average_speed(&self) -> f32 {
62        self.count_average(self.speed_sum)
63    }
64
65    pub fn average_touch_count(&self) -> f32 {
66        self.count_average(self.total_touch_count as f32)
67    }
68
69    pub fn average_horizontal_gap(&self) -> f32 {
70        self.count_average(self.average_horizontal_gap_sum)
71    }
72
73    pub fn average_vertical_gap(&self) -> f32 {
74        self.count_average(self.average_vertical_gap_sum)
75    }
76
77    fn record_event(&mut self, event: &BallCarryEvent) {
78        if let Some(origin) = event.air_dribble_origin {
79            self.labeled_event_counts
80                .increment([air_dribble_origin_label(origin)]);
81        }
82        self.sync_legacy_counts();
83    }
84
85    pub fn event_count_with_labels(&self, labels: &[StatLabel]) -> u32 {
86        self.labeled_event_counts.count_matching(labels)
87    }
88
89    pub fn complete_labeled_event_counts(&self) -> LabeledCounts {
90        LabeledCounts::complete_from_label_sets(
91            &[&AIR_DRIBBLE_ORIGIN_LABELS],
92            &self.labeled_event_counts,
93        )
94    }
95
96    fn sync_legacy_counts(&mut self) {
97        self.count = self.labeled_event_counts.total();
98        self.ground_to_air_count = self
99            .event_count_with_labels(&[air_dribble_origin_label(AirDribbleOrigin::GroundToAir)]);
100        self.wall_to_air_count =
101            self.event_count_with_labels(&[air_dribble_origin_label(AirDribbleOrigin::WallToAir)]);
102    }
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS)]
106#[ts(export)]
107#[serde(rename_all = "snake_case")]
108pub enum AirDribbleOrigin {
109    GroundToAir,
110    WallToAir,
111}
112
113const AIR_DRIBBLE_ORIGIN_LABELS: [StatLabel; 2] = [
114    StatLabel::new("origin", "ground_to_air"),
115    StatLabel::new("origin", "wall_to_air"),
116];
117
118fn air_dribble_origin_label(origin: AirDribbleOrigin) -> StatLabel {
119    StatLabel::new("origin", origin.as_label_value())
120}
121
122impl AirDribbleOrigin {
123    pub fn as_label_value(self) -> &'static str {
124        match self {
125            Self::GroundToAir => "ground_to_air",
126            Self::WallToAir => "wall_to_air",
127        }
128    }
129}
130
131pub(crate) struct AirDribblePolicy;
132
133impl AirDribblePolicy {
134    pub(crate) fn is_sample(
135        player_position: glam::Vec3,
136        ball_position: glam::Vec3,
137        horizontal_gap: f32,
138        vertical_gap: f32,
139    ) -> bool {
140        ball_position.z >= AIR_DRIBBLE_MIN_BALL_Z
141            && player_position.z >= AIR_DRIBBLE_MIN_PLAYER_Z
142            && !player_is_on_wall(player_position)
143            && horizontal_gap <= AIR_DRIBBLE_MAX_HORIZONTAL_GAP
144            && (-AIR_DRIBBLE_MAX_BELOW_CAR_GAP..=AIR_DRIBBLE_MAX_ABOVE_CAR_GAP)
145                .contains(&vertical_gap)
146    }
147
148    pub(crate) fn is_air_touch_position(player_position: glam::Vec3) -> bool {
149        player_position.z > PLAYER_GROUND_Z_THRESHOLD && !player_is_on_wall(player_position)
150    }
151
152    pub(crate) fn kind_requires_airborne(kind: BallCarryKind) -> bool {
153        kind == BallCarryKind::AirDribble
154    }
155
156    pub(crate) fn is_valid_sequence(
157        sequence: &CompletedBallControlSequence<BallCarryKind>,
158    ) -> bool {
159        sequence.kind != BallCarryKind::AirDribble
160            || (sequence.touch_count >= AIR_DRIBBLE_MIN_TOUCHES
161                && sequence.air_touch_count >= AIR_DRIBBLE_MIN_AIR_TOUCHES)
162    }
163
164    pub(crate) fn origin(start_position: glam::Vec3) -> AirDribbleOrigin {
165        if start_position.z >= WALL_TAKEOFF_MIN_Z
166            && (start_position.x.abs() >= SIDE_WALL_START_ABS_X
167                || start_position.y.abs() >= BACK_WALL_START_ABS_Y)
168        {
169            AirDribbleOrigin::WallToAir
170        } else {
171            AirDribbleOrigin::GroundToAir
172        }
173    }
174
175    pub(crate) fn apply_event(stats: &mut AirDribbleStats, event: &BallCarryEvent) {
176        stats.record_event(event);
177        stats.total_time += event.duration;
178        stats.total_straight_line_distance += event.straight_line_distance;
179        stats.total_path_distance += event.path_distance;
180        stats.longest_time = stats.longest_time.max(event.duration);
181        stats.furthest_distance = stats.furthest_distance.max(event.straight_line_distance);
182        stats.fastest_speed = stats.fastest_speed.max(event.average_speed);
183        stats.speed_sum += event.average_speed;
184        stats.average_horizontal_gap_sum += event.average_horizontal_gap;
185        stats.average_vertical_gap_sum += event.average_vertical_gap;
186        stats.total_touch_count += event.touch_count;
187        stats.max_touch_count = stats.max_touch_count.max(event.touch_count);
188    }
189}
190
191#[cfg(test)]
192#[path = "air_dribble_tests.rs"]
193mod tests;