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