subtr_actor/stats/accumulators/
positioning.rs1use super::*;
2
3#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, ts_rs::TS)]
4#[ts(export)]
5pub struct PositioningStats {
6 pub active_game_time: f32,
7 pub tracked_time: f32,
8 pub sum_distance_to_teammates: f32,
9 pub sum_distance_to_ball: f32,
10 pub sum_distance_to_ball_has_possession: f32,
11 pub time_has_possession: f32,
12 pub sum_distance_to_ball_no_possession: f32,
13 pub time_no_possession: f32,
14 pub time_demolished: f32,
15 pub time_no_teammates: f32,
16 pub time_most_back: f32,
17 pub time_most_forward: f32,
18 pub time_mid_role: f32,
19 pub time_other_role: f32,
20 #[serde(rename = "time_defensive_third")]
21 pub time_defensive_zone: f32,
22 #[serde(rename = "time_neutral_third")]
23 pub time_neutral_zone: f32,
24 #[serde(rename = "time_offensive_third")]
25 pub time_offensive_zone: f32,
26 pub time_defensive_half: f32,
27 pub time_offensive_half: f32,
28 pub time_closest_to_ball_team: f32,
29 pub time_closest_to_ball_absolute: f32,
30 pub time_farthest_from_ball: f32,
31 pub time_behind_ball: f32,
32 pub time_level_with_ball: f32,
33 pub time_in_front_of_ball: f32,
34}
35
36#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, ts_rs::TS)]
37#[ts(export)]
38pub struct PositioningTeamStats {
39 pub tracked_time: f32,
40 pub time_closest_to_ball_team: f32,
41 pub time_closest_to_ball_absolute: f32,
42}
43
44impl PositioningTeamStats {
45 fn pct(&self, value: f32) -> f32 {
46 if self.tracked_time == 0.0 {
47 0.0
48 } else {
49 value * 100.0 / self.tracked_time
50 }
51 }
52
53 pub fn closest_to_ball_team_pct(&self) -> f32 {
54 self.pct(self.time_closest_to_ball_team)
55 }
56
57 pub fn closest_to_ball_absolute_pct(&self) -> f32 {
58 self.pct(self.time_closest_to_ball_absolute)
59 }
60}
61
62impl PositioningStats {
63 pub fn average_distance_to_teammates(&self) -> f32 {
64 if self.tracked_time == 0.0 {
65 0.0
66 } else {
67 self.sum_distance_to_teammates / self.tracked_time
68 }
69 }
70
71 pub fn average_distance_to_ball(&self) -> f32 {
72 if self.tracked_time == 0.0 {
73 0.0
74 } else {
75 self.sum_distance_to_ball / self.tracked_time
76 }
77 }
78
79 pub fn average_distance_to_ball_has_possession(&self) -> f32 {
80 if self.time_has_possession == 0.0 {
81 0.0
82 } else {
83 self.sum_distance_to_ball_has_possession / self.time_has_possession
84 }
85 }
86
87 pub fn average_distance_to_ball_no_possession(&self) -> f32 {
88 if self.time_no_possession == 0.0 {
89 0.0
90 } else {
91 self.sum_distance_to_ball_no_possession / self.time_no_possession
92 }
93 }
94
95 fn pct(&self, value: f32) -> f32 {
96 if self.tracked_time == 0.0 {
97 0.0
98 } else {
99 value * 100.0 / self.tracked_time
100 }
101 }
102
103 pub fn most_back_pct(&self) -> f32 {
104 self.pct(self.time_most_back)
105 }
106
107 pub fn most_forward_pct(&self) -> f32 {
108 self.pct(self.time_most_forward)
109 }
110
111 pub fn mid_role_pct(&self) -> f32 {
112 self.pct(self.time_mid_role)
113 }
114
115 pub fn other_role_pct(&self) -> f32 {
116 self.pct(self.time_other_role)
117 }
118
119 pub fn defensive_third_pct(&self) -> f32 {
120 self.pct(self.time_defensive_zone)
121 }
122
123 pub fn neutral_third_pct(&self) -> f32 {
124 self.pct(self.time_neutral_zone)
125 }
126
127 pub fn offensive_third_pct(&self) -> f32 {
128 self.pct(self.time_offensive_zone)
129 }
130
131 pub fn defensive_half_pct(&self) -> f32 {
132 self.pct(self.time_defensive_half)
133 }
134
135 pub fn offensive_half_pct(&self) -> f32 {
136 self.pct(self.time_offensive_half)
137 }
138
139 pub fn closest_to_ball_team_pct(&self) -> f32 {
140 self.pct(self.time_closest_to_ball_team)
141 }
142
143 pub fn closest_to_ball_absolute_pct(&self) -> f32 {
144 self.pct(self.time_closest_to_ball_absolute)
145 }
146
147 pub fn farthest_from_ball_pct(&self) -> f32 {
148 self.pct(self.time_farthest_from_ball)
149 }
150
151 pub fn behind_ball_pct(&self) -> f32 {
152 self.pct(self.time_behind_ball)
153 }
154
155 pub fn level_with_ball_pct(&self) -> f32 {
156 self.pct(self.time_level_with_ball)
157 }
158
159 pub fn in_front_of_ball_pct(&self) -> f32 {
160 self.pct(self.time_in_front_of_ball)
161 }
162}
163
164#[derive(Debug, Clone, Default, PartialEq)]
169pub struct PositioningStatsAccumulator {
170 player_stats: HashMap<PlayerId, PositioningStats>,
171 team_zero_stats: PositioningTeamStats,
172 team_one_stats: PositioningTeamStats,
173}
174
175impl PositioningStatsAccumulator {
176 pub fn new() -> Self {
177 Self::default()
178 }
179
180 pub fn player_stats(&self) -> &HashMap<PlayerId, PositioningStats> {
181 &self.player_stats
182 }
183
184 pub fn team_zero_stats(&self) -> &PositioningTeamStats {
185 &self.team_zero_stats
186 }
187
188 pub fn team_one_stats(&self) -> &PositioningTeamStats {
189 &self.team_one_stats
190 }
191
192 pub fn apply_activity_event(&mut self, event: &PlayerActivityEvent) {
193 let stats = self.player_stats.entry(event.player.clone()).or_default();
194 stats.active_game_time += event.duration;
195 match event.state {
196 ActivityState::Tracked => stats.tracked_time += event.duration,
197 ActivityState::Demolished => stats.time_demolished += event.duration,
198 }
199 }
200
201 pub fn apply_field_third_event(&mut self, event: &FieldThirdEvent) {
202 let stats = self.player_stats.entry(event.player.clone()).or_default();
203 match event.state {
204 FieldThirdState::Defensive => stats.time_defensive_zone += event.duration,
205 FieldThirdState::Neutral => stats.time_neutral_zone += event.duration,
206 FieldThirdState::Offensive => stats.time_offensive_zone += event.duration,
207 }
208 }
209
210 pub fn apply_field_half_event(&mut self, event: &FieldHalfEvent) {
211 let stats = self.player_stats.entry(event.player.clone()).or_default();
212 match event.state {
213 FieldHalfState::Defensive => stats.time_defensive_half += event.duration,
214 FieldHalfState::Offensive => stats.time_offensive_half += event.duration,
215 }
216 }
217
218 pub fn apply_ball_depth_event(&mut self, event: &BallDepthEvent) {
219 let stats = self.player_stats.entry(event.player.clone()).or_default();
220 match event.state {
221 BallDepthState::BehindBall => stats.time_behind_ball += event.duration,
222 BallDepthState::LevelWithBall => stats.time_level_with_ball += event.duration,
223 BallDepthState::AheadOfBall => stats.time_in_front_of_ball += event.duration,
224 }
225 }
226
227 pub fn apply_depth_role_event(&mut self, event: &DepthRoleEvent) {
228 let stats = self.player_stats.entry(event.player.clone()).or_default();
229 match event.state {
230 DepthRoleState::NoTeammates => stats.time_no_teammates += event.duration,
231 DepthRoleState::MostBack => stats.time_most_back += event.duration,
232 DepthRoleState::MostForward => stats.time_most_forward += event.duration,
233 DepthRoleState::Mid => stats.time_mid_role += event.duration,
234 DepthRoleState::Other => stats.time_other_role += event.duration,
235 }
236 }
237
238 pub fn apply_ball_proximity_event(&mut self, event: &BallProximityEvent) {
239 let team_stats = if event.is_team_0 {
240 &mut self.team_zero_stats
241 } else {
242 &mut self.team_one_stats
243 };
244 let stats = self.player_stats.entry(event.player.clone()).or_default();
245 if event.state.closest_to_ball_team {
246 team_stats.tracked_time += event.duration;
249 team_stats.time_closest_to_ball_team += event.duration;
250 stats.time_closest_to_ball_team += event.duration;
251 }
252 if event.state.closest_to_ball_absolute {
253 team_stats.time_closest_to_ball_absolute += event.duration;
254 stats.time_closest_to_ball_absolute += event.duration;
255 }
256 if event.state.farthest_from_ball {
257 stats.time_farthest_from_ball += event.duration;
258 }
259 }
260
261 pub fn apply_signal(&mut self, player: &PlayerId, signal: &PositioningSignalSnapshot) {
265 let stats = self.player_stats.entry(player.clone()).or_default();
266 stats.sum_distance_to_teammates = signal.sum_distance_to_teammates;
267 stats.sum_distance_to_ball = signal.sum_distance_to_ball;
268 stats.sum_distance_to_ball_has_possession = signal.sum_distance_to_ball_has_possession;
269 stats.time_has_possession = signal.time_has_possession;
270 stats.sum_distance_to_ball_no_possession = signal.sum_distance_to_ball_no_possession;
271 stats.time_no_possession = signal.time_no_possession;
272 }
273}
274
275#[cfg(test)]
276#[path = "positioning_stats_tests.rs"]
277mod tests;