subtr_actor/stats/calculators/
continuous_ball_control.rs1use super::*;
2
3#[derive(Debug, Clone, Default, PartialEq)]
4pub struct ContinuousBallControlState {
5 pub completed_sequences: Vec<CompletedBallControlSequence<BallCarryKind>>,
6}
7
8#[derive(Debug, Clone, Copy)]
9pub struct ContinuousBallControlSample<K> {
10 pub kind: K,
11 pub player_position: glam::Vec3,
12 pub horizontal_gap: f32,
13 pub vertical_gap: f32,
14 pub speed: f32,
15}
16
17#[derive(Debug, Clone)]
18pub struct ContinuousBallControlCandidate<K> {
19 pub player_id: PlayerId,
20 pub is_team_0: bool,
21 pub touch_count: u32,
22 pub air_touch_count: u32,
23 pub sample: ContinuousBallControlSample<K>,
24}
25
26#[derive(Debug, Clone)]
27pub struct ContinuousBallControlPlayerStatus {
28 pub player_id: PlayerId,
29 pub is_airborne: bool,
30}
31
32#[derive(Debug, Clone)]
33pub struct ContinuousBallControlTouch {
34 pub player_id: PlayerId,
35 pub is_airborne: bool,
36}
37
38#[derive(Debug, Clone, PartialEq)]
39pub struct CompletedBallControlSequence<K> {
40 pub player_id: PlayerId,
41 pub is_team_0: bool,
42 pub kind: K,
43 pub start_frame: usize,
44 pub end_frame: usize,
45 pub start_time: f32,
46 pub end_time: f32,
47 pub duration: f32,
48 pub straight_line_distance: f32,
49 pub path_distance: f32,
50 pub average_horizontal_gap: f32,
51 pub average_vertical_gap: f32,
52 pub average_speed: f32,
53 pub start_position: glam::Vec3,
54 pub end_position: glam::Vec3,
55 pub touch_count: u32,
56 pub air_touch_count: u32,
57}
58
59#[derive(Debug, Clone)]
60struct ActiveBallControlSequence<K> {
61 player_id: PlayerId,
62 is_team_0: bool,
63 kind: K,
64 start_frame: usize,
65 last_frame: usize,
66 start_time: f32,
67 last_time: f32,
68 start_position: glam::Vec3,
69 last_position: glam::Vec3,
70 duration: f32,
71 path_distance: f32,
72 horizontal_gap_integral: f32,
73 vertical_gap_integral: f32,
74 speed_integral: f32,
75 touch_count: u32,
76 air_touch_count: u32,
77}
78
79#[derive(Debug, Clone)]
80pub struct ContinuousBallControlTracker<K> {
81 active_sequence: Option<ActiveBallControlSequence<K>>,
82 pending_takeoff_touches: HashMap<PlayerId, u32>,
83}
84
85impl<K> Default for ContinuousBallControlTracker<K> {
86 fn default() -> Self {
87 Self {
88 active_sequence: None,
89 pending_takeoff_touches: HashMap::new(),
90 }
91 }
92}
93
94impl<K> ContinuousBallControlTracker<K>
95where
96 K: Copy + PartialEq,
97{
98 fn begin_sequence(
99 frame: &FrameInfo,
100 candidate: ContinuousBallControlCandidate<K>,
101 takeoff_touch_count: u32,
102 ) -> ActiveBallControlSequence<K> {
103 let sample = candidate.sample;
104 ActiveBallControlSequence {
105 player_id: candidate.player_id,
106 is_team_0: candidate.is_team_0,
107 kind: sample.kind,
108 start_frame: frame.frame_number.saturating_sub(1),
109 last_frame: frame.frame_number,
110 start_time: (frame.time - frame.dt).max(0.0),
111 last_time: frame.time,
112 start_position: sample.player_position,
113 last_position: sample.player_position,
114 duration: frame.dt,
115 path_distance: 0.0,
116 horizontal_gap_integral: sample.horizontal_gap * frame.dt,
117 vertical_gap_integral: sample.vertical_gap * frame.dt,
118 speed_integral: sample.speed * frame.dt,
119 touch_count: candidate.touch_count + takeoff_touch_count,
120 air_touch_count: candidate.air_touch_count,
121 }
122 }
123
124 fn extend_sequence(
125 active_sequence: &mut ActiveBallControlSequence<K>,
126 frame: &FrameInfo,
127 sample: ContinuousBallControlSample<K>,
128 touch_count: u32,
129 air_touch_count: u32,
130 ) {
131 active_sequence.duration += frame.dt;
132 active_sequence.path_distance += sample
133 .player_position
134 .distance(active_sequence.last_position);
135 active_sequence.last_position = sample.player_position;
136 active_sequence.last_time = frame.time;
137 active_sequence.last_frame = frame.frame_number;
138 active_sequence.horizontal_gap_integral += sample.horizontal_gap * frame.dt;
139 active_sequence.vertical_gap_integral += sample.vertical_gap * frame.dt;
140 active_sequence.speed_integral += sample.speed * frame.dt;
141 active_sequence.touch_count += touch_count;
142 active_sequence.air_touch_count += air_touch_count;
143 }
144
145 fn complete_sequence(
146 active_sequence: ActiveBallControlSequence<K>,
147 ) -> CompletedBallControlSequence<K> {
148 CompletedBallControlSequence {
149 player_id: active_sequence.player_id,
150 is_team_0: active_sequence.is_team_0,
151 kind: active_sequence.kind,
152 start_frame: active_sequence.start_frame,
153 end_frame: active_sequence.last_frame,
154 start_time: active_sequence.start_time,
155 end_time: active_sequence.last_time,
156 duration: active_sequence.duration,
157 straight_line_distance: active_sequence
158 .start_position
159 .truncate()
160 .distance(active_sequence.last_position.truncate()),
161 path_distance: active_sequence.path_distance,
162 average_horizontal_gap: active_sequence.horizontal_gap_integral
163 / active_sequence.duration,
164 average_vertical_gap: active_sequence.vertical_gap_integral / active_sequence.duration,
165 average_speed: active_sequence.speed_integral / active_sequence.duration,
166 start_position: active_sequence.start_position,
167 end_position: active_sequence.last_position,
168 touch_count: active_sequence.touch_count,
169 air_touch_count: active_sequence.air_touch_count,
170 }
171 }
172
173 fn track_touch_contacts(&mut self, touches: &[ContinuousBallControlTouch]) {
174 for touch in touches {
175 self.pending_takeoff_touches
176 .retain(|player_id, _| player_id == &touch.player_id);
177
178 if !touch.is_airborne {
179 *self
180 .pending_takeoff_touches
181 .entry(touch.player_id.clone())
182 .or_default() += 1;
183 }
184 }
185 }
186
187 fn active_player_is_non_airborne<G>(
188 &self,
189 player_statuses: &[ContinuousBallControlPlayerStatus],
190 requires_airborne_for_kind: G,
191 ) -> bool
192 where
193 G: Fn(K) -> bool,
194 {
195 self.active_sequence
196 .as_ref()
197 .is_some_and(|active_sequence| {
198 requires_airborne_for_kind(active_sequence.kind)
199 && player_statuses
200 .iter()
201 .find(|status| status.player_id == active_sequence.player_id)
202 .is_some_and(|status| !status.is_airborne)
203 })
204 }
205
206 fn finish_active_sequence<F>(
207 &mut self,
208 min_duration_for_kind: F,
209 ) -> Option<CompletedBallControlSequence<K>>
210 where
211 F: Fn(K) -> f32,
212 {
213 let active_sequence = self.active_sequence.take()?;
214 if active_sequence.duration < min_duration_for_kind(active_sequence.kind) {
215 return None;
216 }
217 Some(Self::complete_sequence(active_sequence))
218 }
219
220 pub fn update<F, G>(
221 &mut self,
222 frame: &FrameInfo,
223 candidate: Option<ContinuousBallControlCandidate<K>>,
224 player_statuses: &[ContinuousBallControlPlayerStatus],
225 touches: &[ContinuousBallControlTouch],
226 min_duration_for_kind: F,
227 requires_airborne_for_kind: G,
228 ) -> Vec<CompletedBallControlSequence<K>>
229 where
230 F: Fn(K) -> f32 + Copy,
231 G: Fn(K) -> bool + Copy,
232 {
233 let mut completed = Vec::new();
234 self.track_touch_contacts(touches);
235
236 if self.active_player_is_non_airborne(player_statuses, requires_airborne_for_kind) {
237 if let Some(sequence) = self.finish_active_sequence(min_duration_for_kind) {
238 completed.push(sequence);
239 }
240 }
241
242 let Some(candidate) = candidate else {
243 if let Some(sequence) = self.finish_active_sequence(min_duration_for_kind) {
244 completed.push(sequence);
245 }
246 return completed;
247 };
248
249 let same_sequence = self
250 .active_sequence
251 .as_ref()
252 .is_some_and(|active_sequence| {
253 active_sequence.player_id == candidate.player_id
254 && active_sequence.kind == candidate.sample.kind
255 });
256
257 if same_sequence {
258 if let Some(active_sequence) = self.active_sequence.as_mut() {
259 Self::extend_sequence(
260 active_sequence,
261 frame,
262 candidate.sample,
263 candidate.touch_count,
264 candidate.air_touch_count,
265 );
266 }
267 } else {
268 if let Some(sequence) = self.finish_active_sequence(min_duration_for_kind) {
269 completed.push(sequence);
270 }
271 let takeoff_touch_count = if requires_airborne_for_kind(candidate.sample.kind) {
272 self.pending_takeoff_touches
273 .remove(&candidate.player_id)
274 .unwrap_or(0)
275 } else {
276 0
277 };
278 self.active_sequence =
279 Some(Self::begin_sequence(frame, candidate, takeoff_touch_count));
280 }
281
282 completed
283 }
284
285 pub fn finish<F>(&mut self, min_duration_for_kind: F) -> Option<CompletedBallControlSequence<K>>
286 where
287 F: Fn(K) -> f32,
288 {
289 self.finish_active_sequence(min_duration_for_kind)
290 }
291}