subtr_actor/stats/calculators/
ball_carry.rs1use super::*;
2
3#[derive(Debug, Clone, PartialEq, Serialize, ts_rs::TS)]
5#[ts(export)]
6pub struct BallCarryEvent {
7 #[ts(as = "crate::interop::ts_bindings::RemoteIdTs")]
8 pub player_id: PlayerId,
9 pub is_team_0: bool,
10 pub kind: BallCarryKind,
11 pub start_position: [f32; 3],
12 pub end_position: [f32; 3],
13 pub start_frame: usize,
14 pub end_frame: usize,
15 pub start_time: f32,
16 pub end_time: f32,
17 pub duration: f32,
18 pub straight_line_distance: f32,
19 pub path_distance: f32,
20 pub average_horizontal_gap: f32,
21 pub average_vertical_gap: f32,
22 pub average_speed: f32,
23 pub touch_count: u32,
24 pub air_touch_count: u32,
25 #[serde(skip_serializing_if = "Option::is_none")]
26 pub air_dribble_origin: Option<AirDribbleOrigin>,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS)]
31#[ts(export)]
32#[serde(rename_all = "snake_case")]
33pub enum BallCarryKind {
34 Carry,
35 AirDribble,
36}
37
38#[derive(Debug, Clone, Default)]
40pub struct BallCarryCalculator {
41 carry_events: EventStream<BallCarryEvent>,
42 processed_control_sequence_count: usize,
43}
44
45impl BallCarryCalculator {
46 pub fn new() -> Self {
47 Self::default()
48 }
49
50 pub fn carry_events(&self) -> &[BallCarryEvent] {
51 self.carry_events.all()
52 }
53
54 pub fn new_carry_events(&self) -> &[BallCarryEvent] {
55 self.carry_events.new_events()
56 }
57
58 pub(crate) fn carry_frame_sample(
59 player: &PlayerSample,
60 ball: &BallSample,
61 ) -> Option<ContinuousBallControlSample<BallCarryKind>> {
62 let player_position = player.position()?;
63 let ball_position = ball.position();
64 let horizontal_gap = player_position
65 .truncate()
66 .distance(ball_position.truncate());
67 let vertical_gap = ball_position.z - player_position.z;
68
69 if player_is_on_wall(player_position) {
70 return None;
71 }
72
73 if !(BALL_CARRY_MIN_BALL_Z..=BALL_CARRY_MAX_BALL_Z).contains(&ball_position.z) {
74 return None;
75 }
76
77 if horizontal_gap > BALL_CARRY_MAX_HORIZONTAL_GAP {
78 return None;
79 }
80
81 if !(0.0..=BALL_CARRY_MAX_VERTICAL_GAP).contains(&vertical_gap) {
82 return None;
83 }
84
85 Some(ContinuousBallControlSample {
86 player_position,
87 kind: BallCarryKind::Carry,
88 horizontal_gap,
89 vertical_gap,
90 speed: player.speed().unwrap_or(0.0),
91 })
92 }
93
94 pub(crate) fn kind_requires_airborne(_kind: BallCarryKind) -> bool {
95 false
96 }
97
98 pub(crate) fn control_player_statuses(
99 players: &PlayerFrameState,
100 ) -> Vec<ContinuousBallControlPlayerStatus> {
101 players
102 .players
103 .iter()
104 .filter_map(|player| {
105 Some(ContinuousBallControlPlayerStatus {
106 player_id: player.player_id.clone(),
107 is_airborne: AirDribblePolicy::is_air_touch_position(player.position()?),
108 })
109 })
110 .collect()
111 }
112
113 pub(crate) fn control_touches(
114 touch_state: &TouchState,
115 players: &PlayerFrameState,
116 ) -> Vec<ContinuousBallControlTouch> {
117 touch_state
118 .touch_events
119 .iter()
120 .filter_map(|touch| {
121 let player_id = touch.player.clone()?;
122 let player = players
123 .players
124 .iter()
125 .find(|player| player.player_id == player_id)?;
126 Some(ContinuousBallControlTouch {
127 player_id,
128 is_airborne: AirDribblePolicy::is_air_touch_position(player.position()?),
129 })
130 })
131 .collect()
132 }
133
134 pub(crate) fn min_duration_for_kind(kind: BallCarryKind) -> f32 {
135 match kind {
136 BallCarryKind::Carry => BALL_CARRY_MIN_DURATION,
137 BallCarryKind::AirDribble => AIR_DRIBBLE_MIN_DURATION,
138 }
139 }
140
141 pub(crate) fn control_candidate(
142 ball: &BallFrameState,
143 players: &PlayerFrameState,
144 live_play_state: &LivePlayState,
145 touch_state: &TouchState,
146 ) -> Option<ContinuousBallControlCandidate<BallCarryKind>> {
147 if !live_play_state.is_live_play {
148 return None;
149 }
150 let ball = ball.sample()?;
151 let player_id = touch_state.last_touch_player.as_ref()?;
152 let touch_count = touch_state
153 .touch_events
154 .iter()
155 .filter(|event| event.player.as_ref() == Some(player_id))
156 .count() as u32;
157 players
158 .players
159 .iter()
160 .find(|player| &player.player_id == player_id)
161 .and_then(|player| {
162 Self::carry_frame_sample(player, ball).map(|sample| {
163 ContinuousBallControlCandidate {
164 player_id: player.player_id.clone(),
165 is_team_0: player.is_team_0,
166 touch_count,
167 air_touch_count: 0,
168 sample,
169 }
170 })
171 })
172 }
173
174 fn event_from_sequence(
175 sequence: CompletedBallControlSequence<BallCarryKind>,
176 ) -> BallCarryEvent {
177 BallCarryEvent {
178 player_id: sequence.player_id,
179 is_team_0: sequence.is_team_0,
180 kind: sequence.kind,
181 start_position: sequence.start_position.to_array(),
182 end_position: sequence.end_position.to_array(),
183 start_frame: sequence.start_frame,
184 end_frame: sequence.end_frame,
185 start_time: sequence.start_time,
186 end_time: sequence.end_time,
187 duration: sequence.duration,
188 straight_line_distance: sequence.straight_line_distance,
189 path_distance: sequence.path_distance,
190 average_horizontal_gap: sequence.average_horizontal_gap,
191 average_vertical_gap: sequence.average_vertical_gap,
192 average_speed: sequence.average_speed,
193 touch_count: sequence.touch_count,
194 air_touch_count: sequence.air_touch_count,
195 air_dribble_origin: None,
196 }
197 }
198
199 fn record_carry_event(&mut self, event: BallCarryEvent) {
200 self.carry_events.push(event);
201 }
202
203 pub fn update(&mut self, control_state: &ContinuousBallControlState) -> SubtrActorResult<()> {
204 self.carry_events.begin_update();
205 for sequence in control_state
206 .completed_sequences
207 .iter()
208 .skip(self.processed_control_sequence_count)
209 .cloned()
210 {
211 self.record_carry_event(Self::event_from_sequence(sequence));
212 }
213 self.processed_control_sequence_count = control_state.completed_sequences.len();
214 Ok(())
215 }
216}
217
218#[cfg(test)]
219#[path = "ball_carry_tests.rs"]
220mod tests;