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 sample: ContinuousBallControlSample<K>,
22}
23
24#[derive(Debug, Clone, PartialEq)]
25pub struct CompletedBallControlSequence<K> {
26 pub player_id: PlayerId,
27 pub is_team_0: bool,
28 pub kind: K,
29 pub start_frame: usize,
30 pub end_frame: usize,
31 pub start_time: f32,
32 pub end_time: f32,
33 pub duration: f32,
34 pub straight_line_distance: f32,
35 pub path_distance: f32,
36 pub average_horizontal_gap: f32,
37 pub average_vertical_gap: f32,
38 pub average_speed: f32,
39}
40
41#[derive(Debug, Clone)]
42struct ActiveBallControlSequence<K> {
43 player_id: PlayerId,
44 is_team_0: bool,
45 kind: K,
46 start_frame: usize,
47 last_frame: usize,
48 start_time: f32,
49 last_time: f32,
50 start_position: glam::Vec3,
51 last_position: glam::Vec3,
52 duration: f32,
53 path_distance: f32,
54 horizontal_gap_integral: f32,
55 vertical_gap_integral: f32,
56 speed_integral: f32,
57}
58
59#[derive(Debug, Clone)]
60pub struct ContinuousBallControlTracker<K> {
61 active_sequence: Option<ActiveBallControlSequence<K>>,
62}
63
64impl<K> Default for ContinuousBallControlTracker<K> {
65 fn default() -> Self {
66 Self {
67 active_sequence: None,
68 }
69 }
70}
71
72impl<K> ContinuousBallControlTracker<K>
73where
74 K: Copy + PartialEq,
75{
76 fn begin_sequence(
77 frame: &FrameInfo,
78 candidate: ContinuousBallControlCandidate<K>,
79 ) -> ActiveBallControlSequence<K> {
80 let sample = candidate.sample;
81 ActiveBallControlSequence {
82 player_id: candidate.player_id,
83 is_team_0: candidate.is_team_0,
84 kind: sample.kind,
85 start_frame: frame.frame_number.saturating_sub(1),
86 last_frame: frame.frame_number,
87 start_time: (frame.time - frame.dt).max(0.0),
88 last_time: frame.time,
89 start_position: sample.player_position,
90 last_position: sample.player_position,
91 duration: frame.dt,
92 path_distance: 0.0,
93 horizontal_gap_integral: sample.horizontal_gap * frame.dt,
94 vertical_gap_integral: sample.vertical_gap * frame.dt,
95 speed_integral: sample.speed * frame.dt,
96 }
97 }
98
99 fn extend_sequence(
100 active_sequence: &mut ActiveBallControlSequence<K>,
101 frame: &FrameInfo,
102 sample: ContinuousBallControlSample<K>,
103 ) {
104 active_sequence.duration += frame.dt;
105 active_sequence.path_distance += sample
106 .player_position
107 .distance(active_sequence.last_position);
108 active_sequence.last_position = sample.player_position;
109 active_sequence.last_time = frame.time;
110 active_sequence.last_frame = frame.frame_number;
111 active_sequence.horizontal_gap_integral += sample.horizontal_gap * frame.dt;
112 active_sequence.vertical_gap_integral += sample.vertical_gap * frame.dt;
113 active_sequence.speed_integral += sample.speed * frame.dt;
114 }
115
116 fn complete_sequence(
117 active_sequence: ActiveBallControlSequence<K>,
118 ) -> CompletedBallControlSequence<K> {
119 CompletedBallControlSequence {
120 player_id: active_sequence.player_id,
121 is_team_0: active_sequence.is_team_0,
122 kind: active_sequence.kind,
123 start_frame: active_sequence.start_frame,
124 end_frame: active_sequence.last_frame,
125 start_time: active_sequence.start_time,
126 end_time: active_sequence.last_time,
127 duration: active_sequence.duration,
128 straight_line_distance: active_sequence
129 .start_position
130 .truncate()
131 .distance(active_sequence.last_position.truncate()),
132 path_distance: active_sequence.path_distance,
133 average_horizontal_gap: active_sequence.horizontal_gap_integral
134 / active_sequence.duration,
135 average_vertical_gap: active_sequence.vertical_gap_integral / active_sequence.duration,
136 average_speed: active_sequence.speed_integral / active_sequence.duration,
137 }
138 }
139
140 fn finish_active_sequence<F>(
141 &mut self,
142 min_duration_for_kind: F,
143 ) -> Option<CompletedBallControlSequence<K>>
144 where
145 F: Fn(K) -> f32,
146 {
147 let active_sequence = self.active_sequence.take()?;
148 if active_sequence.duration < min_duration_for_kind(active_sequence.kind) {
149 return None;
150 }
151 Some(Self::complete_sequence(active_sequence))
152 }
153
154 pub fn update<F>(
155 &mut self,
156 frame: &FrameInfo,
157 candidate: Option<ContinuousBallControlCandidate<K>>,
158 min_duration_for_kind: F,
159 ) -> Vec<CompletedBallControlSequence<K>>
160 where
161 F: Fn(K) -> f32 + Copy,
162 {
163 let mut completed = Vec::new();
164 let Some(candidate) = candidate else {
165 if let Some(sequence) = self.finish_active_sequence(min_duration_for_kind) {
166 completed.push(sequence);
167 }
168 return completed;
169 };
170
171 let same_sequence = self
172 .active_sequence
173 .as_ref()
174 .is_some_and(|active_sequence| {
175 active_sequence.player_id == candidate.player_id
176 && active_sequence.kind == candidate.sample.kind
177 });
178
179 if same_sequence {
180 if let Some(active_sequence) = self.active_sequence.as_mut() {
181 Self::extend_sequence(active_sequence, frame, candidate.sample);
182 }
183 } else {
184 if let Some(sequence) = self.finish_active_sequence(min_duration_for_kind) {
185 completed.push(sequence);
186 }
187 self.active_sequence = Some(Self::begin_sequence(frame, candidate));
188 }
189
190 completed
191 }
192
193 pub fn finish<F>(&mut self, min_duration_for_kind: F) -> Option<CompletedBallControlSequence<K>>
194 where
195 F: Fn(K) -> f32,
196 {
197 self.finish_active_sequence(min_duration_for_kind)
198 }
199}