subtr_actor/stats/calculators/
air_dribble.rs1use 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;