Skip to main content

subtr_actor/stats/calculators/
continuous_ball_control.rs

1use 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}