1use super::*;
2
3const GOAL_CAUGHT_AHEAD_MAX_BALL_Y: f32 = -1200.0;
4const GOAL_CAUGHT_AHEAD_MIN_PLAYER_Y: f32 = -250.0;
5const GOAL_CAUGHT_AHEAD_MIN_BALL_DELTA_Y: f32 = 2200.0;
6const DEFAULT_LEVEL_BALL_DEPTH_MARGIN: f32 = 150.0;
7
8#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, ts_rs::TS)]
9#[ts(export)]
10pub struct PositioningStats {
11 pub active_game_time: f32,
12 pub tracked_time: f32,
13 pub sum_distance_to_teammates: f32,
14 pub sum_distance_to_ball: f32,
15 pub sum_distance_to_ball_has_possession: f32,
16 pub time_has_possession: f32,
17 pub sum_distance_to_ball_no_possession: f32,
18 pub time_no_possession: f32,
19 pub time_demolished: f32,
20 pub time_no_teammates: f32,
21 pub time_most_back: f32,
22 pub time_most_forward: f32,
23 pub time_mid_role: f32,
24 pub time_other_role: f32,
25 #[serde(rename = "time_defensive_third")]
26 pub time_defensive_zone: f32,
27 #[serde(rename = "time_neutral_third")]
28 pub time_neutral_zone: f32,
29 #[serde(rename = "time_offensive_third")]
30 pub time_offensive_zone: f32,
31 pub time_defensive_half: f32,
32 pub time_offensive_half: f32,
33 pub time_closest_to_ball: f32,
34 pub time_farthest_from_ball: f32,
35 pub time_behind_ball: f32,
36 pub time_level_with_ball: f32,
37 pub time_in_front_of_ball: f32,
38 pub times_caught_ahead_of_play_on_conceded_goals: u32,
39}
40
41#[derive(Debug, Clone, PartialEq, Serialize, ts_rs::TS)]
42#[ts(export)]
43pub struct PositioningEvent {
44 pub time: f32,
45 pub frame: usize,
46 #[ts(as = "crate::ts_bindings::RemoteIdTs")]
47 pub player: PlayerId,
48 pub is_team_0: bool,
49 pub active_game_time: f32,
50 pub tracked_time: f32,
51 pub sum_distance_to_teammates: f32,
52 pub sum_distance_to_ball: f32,
53 pub sum_distance_to_ball_has_possession: f32,
54 pub time_has_possession: f32,
55 pub sum_distance_to_ball_no_possession: f32,
56 pub time_no_possession: f32,
57 pub time_demolished: f32,
58 pub time_no_teammates: f32,
59 pub time_most_back: f32,
60 pub time_most_forward: f32,
61 pub time_mid_role: f32,
62 pub time_other_role: f32,
63 #[serde(rename = "time_defensive_third")]
64 pub time_defensive_zone: f32,
65 #[serde(rename = "time_neutral_third")]
66 pub time_neutral_zone: f32,
67 #[serde(rename = "time_offensive_third")]
68 pub time_offensive_zone: f32,
69 pub time_defensive_half: f32,
70 pub time_offensive_half: f32,
71 pub time_closest_to_ball: f32,
72 pub time_farthest_from_ball: f32,
73 pub time_behind_ball: f32,
74 pub time_level_with_ball: f32,
75 pub time_in_front_of_ball: f32,
76 pub times_caught_ahead_of_play_on_conceded_goals: u32,
77}
78
79impl PositioningEvent {
80 fn new(frame: &FrameInfo, player: PlayerId, is_team_0: bool) -> Self {
81 Self {
82 time: frame.time,
83 frame: frame.frame_number,
84 player,
85 is_team_0,
86 active_game_time: 0.0,
87 tracked_time: 0.0,
88 sum_distance_to_teammates: 0.0,
89 sum_distance_to_ball: 0.0,
90 sum_distance_to_ball_has_possession: 0.0,
91 time_has_possession: 0.0,
92 sum_distance_to_ball_no_possession: 0.0,
93 time_no_possession: 0.0,
94 time_demolished: 0.0,
95 time_no_teammates: 0.0,
96 time_most_back: 0.0,
97 time_most_forward: 0.0,
98 time_mid_role: 0.0,
99 time_other_role: 0.0,
100 time_defensive_zone: 0.0,
101 time_neutral_zone: 0.0,
102 time_offensive_zone: 0.0,
103 time_defensive_half: 0.0,
104 time_offensive_half: 0.0,
105 time_closest_to_ball: 0.0,
106 time_farthest_from_ball: 0.0,
107 time_behind_ball: 0.0,
108 time_level_with_ball: 0.0,
109 time_in_front_of_ball: 0.0,
110 times_caught_ahead_of_play_on_conceded_goals: 0,
111 }
112 }
113
114 fn has_delta(&self) -> bool {
115 self.active_game_time != 0.0
116 || self.tracked_time != 0.0
117 || self.sum_distance_to_teammates != 0.0
118 || self.sum_distance_to_ball != 0.0
119 || self.sum_distance_to_ball_has_possession != 0.0
120 || self.time_has_possession != 0.0
121 || self.sum_distance_to_ball_no_possession != 0.0
122 || self.time_no_possession != 0.0
123 || self.time_demolished != 0.0
124 || self.time_no_teammates != 0.0
125 || self.time_most_back != 0.0
126 || self.time_most_forward != 0.0
127 || self.time_mid_role != 0.0
128 || self.time_other_role != 0.0
129 || self.time_defensive_zone != 0.0
130 || self.time_neutral_zone != 0.0
131 || self.time_offensive_zone != 0.0
132 || self.time_defensive_half != 0.0
133 || self.time_offensive_half != 0.0
134 || self.time_closest_to_ball != 0.0
135 || self.time_farthest_from_ball != 0.0
136 || self.time_behind_ball != 0.0
137 || self.time_level_with_ball != 0.0
138 || self.time_in_front_of_ball != 0.0
139 || self.times_caught_ahead_of_play_on_conceded_goals != 0
140 }
141}
142
143impl PositioningStats {
144 pub fn average_distance_to_teammates(&self) -> f32 {
145 if self.tracked_time == 0.0 {
146 0.0
147 } else {
148 self.sum_distance_to_teammates / self.tracked_time
149 }
150 }
151
152 pub fn average_distance_to_ball(&self) -> f32 {
153 if self.tracked_time == 0.0 {
154 0.0
155 } else {
156 self.sum_distance_to_ball / self.tracked_time
157 }
158 }
159
160 pub fn average_distance_to_ball_has_possession(&self) -> f32 {
161 if self.time_has_possession == 0.0 {
162 0.0
163 } else {
164 self.sum_distance_to_ball_has_possession / self.time_has_possession
165 }
166 }
167
168 pub fn average_distance_to_ball_no_possession(&self) -> f32 {
169 if self.time_no_possession == 0.0 {
170 0.0
171 } else {
172 self.sum_distance_to_ball_no_possession / self.time_no_possession
173 }
174 }
175
176 fn pct(&self, value: f32) -> f32 {
177 if self.tracked_time == 0.0 {
178 0.0
179 } else {
180 value * 100.0 / self.tracked_time
181 }
182 }
183
184 pub fn most_back_pct(&self) -> f32 {
185 self.pct(self.time_most_back)
186 }
187
188 pub fn most_forward_pct(&self) -> f32 {
189 self.pct(self.time_most_forward)
190 }
191
192 pub fn mid_role_pct(&self) -> f32 {
193 self.pct(self.time_mid_role)
194 }
195
196 pub fn other_role_pct(&self) -> f32 {
197 self.pct(self.time_other_role)
198 }
199
200 pub fn defensive_third_pct(&self) -> f32 {
201 self.pct(self.time_defensive_zone)
202 }
203
204 pub fn neutral_third_pct(&self) -> f32 {
205 self.pct(self.time_neutral_zone)
206 }
207
208 pub fn offensive_third_pct(&self) -> f32 {
209 self.pct(self.time_offensive_zone)
210 }
211
212 pub fn defensive_zone_pct(&self) -> f32 {
213 self.defensive_third_pct()
214 }
215
216 pub fn neutral_zone_pct(&self) -> f32 {
217 self.neutral_third_pct()
218 }
219
220 pub fn offensive_zone_pct(&self) -> f32 {
221 self.offensive_third_pct()
222 }
223
224 pub fn defensive_half_pct(&self) -> f32 {
225 self.pct(self.time_defensive_half)
226 }
227
228 pub fn offensive_half_pct(&self) -> f32 {
229 self.pct(self.time_offensive_half)
230 }
231
232 pub fn closest_to_ball_pct(&self) -> f32 {
233 self.pct(self.time_closest_to_ball)
234 }
235
236 pub fn farthest_from_ball_pct(&self) -> f32 {
237 self.pct(self.time_farthest_from_ball)
238 }
239
240 pub fn behind_ball_pct(&self) -> f32 {
241 self.pct(self.time_behind_ball)
242 }
243
244 pub fn level_with_ball_pct(&self) -> f32 {
245 self.pct(self.time_level_with_ball)
246 }
247
248 pub fn in_front_of_ball_pct(&self) -> f32 {
249 self.pct(self.time_in_front_of_ball)
250 }
251}
252
253#[derive(Debug, Clone)]
254pub struct PositioningCalculatorConfig {
255 pub most_back_forward_threshold_y: f32,
256 pub level_ball_depth_margin: f32,
257}
258
259impl Default for PositioningCalculatorConfig {
260 fn default() -> Self {
261 Self {
262 most_back_forward_threshold_y: DEFAULT_MOST_BACK_FORWARD_THRESHOLD_Y,
263 level_ball_depth_margin: DEFAULT_LEVEL_BALL_DEPTH_MARGIN,
264 }
265 }
266}
267
268#[derive(Debug, Clone, Default)]
269pub struct PositioningCalculator {
270 config: PositioningCalculatorConfig,
271 player_stats: HashMap<PlayerId, PositioningStats>,
272 previous_ball_position: Option<glam::Vec3>,
273 previous_player_positions: HashMap<PlayerId, glam::Vec3>,
274 events: Vec<PositioningEvent>,
275}
276
277impl PositioningCalculator {
278 pub fn new() -> Self {
279 Self::default()
280 }
281
282 pub fn with_config(config: PositioningCalculatorConfig) -> Self {
283 Self {
284 config,
285 ..Self::default()
286 }
287 }
288
289 pub fn config(&self) -> &PositioningCalculatorConfig {
290 &self.config
291 }
292
293 pub fn player_stats(&self) -> &HashMap<PlayerId, PositioningStats> {
294 &self.player_stats
295 }
296
297 pub fn events(&self) -> &[PositioningEvent] {
298 &self.events
299 }
300
301 fn event_delta<'a>(
302 deltas: &'a mut HashMap<PlayerId, PositioningEvent>,
303 frame: &FrameInfo,
304 player_id: &PlayerId,
305 is_team_0: bool,
306 ) -> &'a mut PositioningEvent {
307 deltas
308 .entry(player_id.clone())
309 .or_insert_with(|| PositioningEvent::new(frame, player_id.clone(), is_team_0))
310 }
311
312 fn record_goal_positioning_events(
313 &mut self,
314 frame: &FrameInfo,
315 players: &PlayerFrameState,
316 events: &FrameEventsState,
317 ball_position: glam::Vec3,
318 event_deltas: &mut HashMap<PlayerId, PositioningEvent>,
319 ) {
320 for goal_event in &events.goal_events {
321 let defending_team_is_team_0 = !goal_event.scoring_team_is_team_0;
322 let normalized_ball_y = normalized_y(defending_team_is_team_0, ball_position);
323 if normalized_ball_y > GOAL_CAUGHT_AHEAD_MAX_BALL_Y {
324 continue;
325 }
326
327 for player in players
328 .players
329 .iter()
330 .filter(|player| player.is_team_0 == defending_team_is_team_0)
331 {
332 let Some(position) = player.position() else {
333 continue;
334 };
335 let normalized_player_y = normalized_y(defending_team_is_team_0, position);
336 if normalized_player_y < GOAL_CAUGHT_AHEAD_MIN_PLAYER_Y {
337 continue;
338 }
339 if normalized_player_y - normalized_ball_y < GOAL_CAUGHT_AHEAD_MIN_BALL_DELTA_Y {
340 continue;
341 }
342
343 self.player_stats
344 .entry(player.player_id.clone())
345 .or_default()
346 .times_caught_ahead_of_play_on_conceded_goals += 1;
347 Self::event_delta(event_deltas, frame, &player.player_id, player.is_team_0)
348 .times_caught_ahead_of_play_on_conceded_goals += 1;
349 }
350 }
351 }
352
353 #[allow(clippy::too_many_arguments)]
354 fn process_sample(
355 &mut self,
356 frame: &FrameInfo,
357 gameplay: &GameplayState,
358 ball: &BallFrameState,
359 players: &PlayerFrameState,
360 events: &FrameEventsState,
361 live_play: bool,
362 possession_player_before_sample: Option<&PlayerId>,
363 ) -> SubtrActorResult<()> {
364 if frame.dt == 0.0 {
365 if let Some(ball) = ball.sample() {
366 self.previous_ball_position = Some(ball.position());
367 }
368 for player in &players.players {
369 if let Some(position) = player.position() {
370 self.previous_player_positions
371 .insert(player.player_id.clone(), position);
372 }
373 }
374 return Ok(());
375 }
376
377 let Some(ball) = ball.sample() else {
378 return Ok(());
379 };
380 let ball_position = ball.position();
381 let mut event_deltas = HashMap::new();
382 if !events.goal_events.is_empty() {
383 self.record_goal_positioning_events(
384 frame,
385 players,
386 events,
387 ball_position,
388 &mut event_deltas,
389 );
390 }
391 let demoed_players: HashSet<_> = events
392 .active_demos
393 .iter()
394 .map(|demo| demo.victim.clone())
395 .collect();
396
397 for player in &players.players {
398 let is_demoed = demoed_players.contains(&player.player_id);
399 if live_play && is_demoed {
400 let stats = self
401 .player_stats
402 .entry(player.player_id.clone())
403 .or_default();
404 stats.active_game_time += frame.dt;
405 stats.time_demolished += frame.dt;
406 let delta = Self::event_delta(
407 &mut event_deltas,
408 frame,
409 &player.player_id,
410 player.is_team_0,
411 );
412 delta.active_game_time += frame.dt;
413 delta.time_demolished += frame.dt;
414 continue;
415 }
416
417 let Some(position) = player.position() else {
418 continue;
419 };
420 let previous_position = self
421 .previous_player_positions
422 .get(&player.player_id)
423 .copied()
424 .unwrap_or(position);
425 let previous_ball_position = self.previous_ball_position.unwrap_or(ball_position);
426 let normalized_position_y = normalized_y(player.is_team_0, position);
427 let normalized_previous_position_y = normalized_y(player.is_team_0, previous_position);
428 let normalized_ball_y = normalized_y(player.is_team_0, ball_position);
429 let normalized_previous_ball_y = normalized_y(player.is_team_0, previous_ball_position);
430 let stats = self
431 .player_stats
432 .entry(player.player_id.clone())
433 .or_default();
434
435 if live_play {
436 stats.active_game_time += frame.dt;
437 stats.tracked_time += frame.dt;
438 let distance_to_ball = position.distance(ball_position);
439 stats.sum_distance_to_ball += distance_to_ball * frame.dt;
440 let delta = Self::event_delta(
441 &mut event_deltas,
442 frame,
443 &player.player_id,
444 player.is_team_0,
445 );
446 delta.active_game_time += frame.dt;
447 delta.tracked_time += frame.dt;
448 delta.sum_distance_to_ball += distance_to_ball * frame.dt;
449
450 if possession_player_before_sample == Some(&player.player_id) {
451 stats.time_has_possession += frame.dt;
452 stats.sum_distance_to_ball_has_possession += distance_to_ball * frame.dt;
453 delta.time_has_possession += frame.dt;
454 delta.sum_distance_to_ball_has_possession += distance_to_ball * frame.dt;
455 } else if possession_player_before_sample.is_some() {
456 stats.time_no_possession += frame.dt;
457 stats.sum_distance_to_ball_no_possession += distance_to_ball * frame.dt;
458 delta.time_no_possession += frame.dt;
459 delta.sum_distance_to_ball_no_possession += distance_to_ball * frame.dt;
460 }
461
462 let defensive_zone_fraction = interval_fraction_below_threshold(
463 normalized_previous_position_y,
464 normalized_position_y,
465 -FIELD_ZONE_BOUNDARY_Y,
466 );
467 let offensive_zone_fraction = interval_fraction_above_threshold(
468 normalized_previous_position_y,
469 normalized_position_y,
470 FIELD_ZONE_BOUNDARY_Y,
471 );
472 let neutral_zone_fraction = interval_fraction_in_scalar_range(
473 normalized_previous_position_y,
474 normalized_position_y,
475 -FIELD_ZONE_BOUNDARY_Y,
476 FIELD_ZONE_BOUNDARY_Y,
477 );
478 stats.time_defensive_zone += frame.dt * defensive_zone_fraction;
479 stats.time_neutral_zone += frame.dt * neutral_zone_fraction;
480 stats.time_offensive_zone += frame.dt * offensive_zone_fraction;
481 delta.time_defensive_zone += frame.dt * defensive_zone_fraction;
482 delta.time_neutral_zone += frame.dt * neutral_zone_fraction;
483 delta.time_offensive_zone += frame.dt * offensive_zone_fraction;
484
485 let defensive_half_fraction = interval_fraction_below_threshold(
486 normalized_previous_position_y,
487 normalized_position_y,
488 0.0,
489 );
490 stats.time_defensive_half += frame.dt * defensive_half_fraction;
491 stats.time_offensive_half += frame.dt * (1.0 - defensive_half_fraction);
492 delta.time_defensive_half += frame.dt * defensive_half_fraction;
493 delta.time_offensive_half += frame.dt * (1.0 - defensive_half_fraction);
494
495 let previous_ball_delta =
496 normalized_previous_position_y - normalized_previous_ball_y;
497 let current_ball_delta = normalized_position_y - normalized_ball_y;
498 let (behind_ball_fraction, level_ball_fraction, in_front_ball_fraction) =
499 ball_depth_fractions(
500 self.config.level_ball_depth_margin,
501 previous_ball_delta,
502 current_ball_delta,
503 );
504 stats.time_behind_ball += frame.dt * behind_ball_fraction;
505 stats.time_level_with_ball += frame.dt * level_ball_fraction;
506 stats.time_in_front_of_ball += frame.dt * in_front_ball_fraction;
507 delta.time_behind_ball += frame.dt * behind_ball_fraction;
508 delta.time_level_with_ball += frame.dt * level_ball_fraction;
509 delta.time_in_front_of_ball += frame.dt * in_front_ball_fraction;
510 }
511 }
512
513 if live_play {
514 for is_team_0 in [true, false] {
515 let team_present_player_count = players
516 .players
517 .iter()
518 .filter(|player| player.is_team_0 == is_team_0)
519 .count();
520 let team_roster_count = gameplay.current_in_game_team_player_count(is_team_0).max(
521 players
522 .players
523 .iter()
524 .filter(|player| player.is_team_0 == is_team_0)
525 .count(),
526 );
527 let team_players: Vec<_> = players
528 .players
529 .iter()
530 .filter(|player| player.is_team_0 == is_team_0)
531 .filter(|player| !demoed_players.contains(&player.player_id))
532 .filter_map(|player| player.position().map(|position| (player, position)))
533 .collect();
534
535 if team_players.is_empty() {
536 continue;
537 }
538
539 for (player, position) in &team_players {
540 let teammate_distance_sum: f32 = team_players
541 .iter()
542 .filter(|(other_player, _)| other_player.player_id != player.player_id)
543 .map(|(_, other_position)| position.distance(*other_position))
544 .sum();
545 let teammate_count = team_players.len().saturating_sub(1);
546 if teammate_count > 0 {
547 let stats = self
548 .player_stats
549 .entry(player.player_id.clone())
550 .or_default();
551 stats.sum_distance_to_teammates +=
552 teammate_distance_sum * frame.dt / teammate_count as f32;
553 Self::event_delta(
554 &mut event_deltas,
555 frame,
556 &player.player_id,
557 player.is_team_0,
558 )
559 .sum_distance_to_teammates +=
560 teammate_distance_sum * frame.dt / teammate_count as f32;
561 }
562 }
563
564 if team_roster_count < 2
565 || team_present_player_count < team_roster_count
566 || team_players.len() < 2
567 {
568 for (player, _) in &team_players {
569 self.player_stats
570 .entry(player.player_id.clone())
571 .or_default()
572 .time_no_teammates += frame.dt;
573 Self::event_delta(
574 &mut event_deltas,
575 frame,
576 &player.player_id,
577 player.is_team_0,
578 )
579 .time_no_teammates += frame.dt;
580 }
581 } else {
582 let mut sorted_team: Vec<_> = team_players
583 .iter()
584 .map(|(info, pos)| (info.player_id.clone(), normalized_y(is_team_0, *pos)))
585 .collect();
586 sorted_team.sort_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap());
587
588 let team_spread = sorted_team.last().map(|(_, y)| *y).unwrap_or(0.0)
589 - sorted_team.first().map(|(_, y)| *y).unwrap_or(0.0);
590
591 if team_spread <= self.config.most_back_forward_threshold_y {
592 for (player_id, _) in &sorted_team {
593 self.player_stats
594 .entry(player_id.clone())
595 .or_default()
596 .time_other_role += frame.dt;
597 Self::event_delta(&mut event_deltas, frame, player_id, is_team_0)
598 .time_other_role += frame.dt;
599 }
600 } else {
601 let min_y = sorted_team.first().map(|(_, y)| *y).unwrap_or(0.0);
602 let max_y = sorted_team.last().map(|(_, y)| *y).unwrap_or(0.0);
603 let can_assign_mid_role = sorted_team.len() == 3;
604
605 for (player_id, y) in &sorted_team {
606 let near_back =
607 (*y - min_y) <= self.config.most_back_forward_threshold_y;
608 let near_front =
609 (max_y - *y) <= self.config.most_back_forward_threshold_y;
610
611 if near_back && !near_front {
612 self.player_stats
613 .entry(player_id.clone())
614 .or_default()
615 .time_most_back += frame.dt;
616 Self::event_delta(
617 &mut event_deltas,
618 frame,
619 player_id,
620 is_team_0,
621 )
622 .time_most_back += frame.dt;
623 } else if near_front && !near_back {
624 self.player_stats
625 .entry(player_id.clone())
626 .or_default()
627 .time_most_forward += frame.dt;
628 Self::event_delta(
629 &mut event_deltas,
630 frame,
631 player_id,
632 is_team_0,
633 )
634 .time_most_forward += frame.dt;
635 } else if can_assign_mid_role {
636 self.player_stats
637 .entry(player_id.clone())
638 .or_default()
639 .time_mid_role += frame.dt;
640 Self::event_delta(
641 &mut event_deltas,
642 frame,
643 player_id,
644 is_team_0,
645 )
646 .time_mid_role += frame.dt;
647 } else {
648 self.player_stats
649 .entry(player_id.clone())
650 .or_default()
651 .time_other_role += frame.dt;
652 Self::event_delta(
653 &mut event_deltas,
654 frame,
655 player_id,
656 is_team_0,
657 )
658 .time_other_role += frame.dt;
659 }
660 }
661 }
662 }
663
664 if let Some((closest_player, _)) = team_players.iter().min_by(|(_, a), (_, b)| {
665 a.distance(ball_position)
666 .partial_cmp(&b.distance(ball_position))
667 .unwrap()
668 }) {
669 self.player_stats
670 .entry(closest_player.player_id.clone())
671 .or_default()
672 .time_closest_to_ball += frame.dt;
673 Self::event_delta(
674 &mut event_deltas,
675 frame,
676 &closest_player.player_id,
677 closest_player.is_team_0,
678 )
679 .time_closest_to_ball += frame.dt;
680 }
681
682 if let Some((farthest_player, _)) = team_players.iter().max_by(|(_, a), (_, b)| {
683 a.distance(ball_position)
684 .partial_cmp(&b.distance(ball_position))
685 .unwrap()
686 }) {
687 self.player_stats
688 .entry(farthest_player.player_id.clone())
689 .or_default()
690 .time_farthest_from_ball += frame.dt;
691 Self::event_delta(
692 &mut event_deltas,
693 frame,
694 &farthest_player.player_id,
695 farthest_player.is_team_0,
696 )
697 .time_farthest_from_ball += frame.dt;
698 }
699 }
700 }
701
702 let mut frame_events: Vec<_> = event_deltas
703 .into_values()
704 .filter(PositioningEvent::has_delta)
705 .collect();
706 frame_events.sort_by(|left, right| {
707 format!("{:?}", left.player).cmp(&format!("{:?}", right.player))
708 });
709 self.events.extend(frame_events);
710
711 self.previous_ball_position = Some(ball_position);
712 for player in &players.players {
713 if let Some(position) = player.position() {
714 self.previous_player_positions
715 .insert(player.player_id.clone(), position);
716 }
717 }
718
719 Ok(())
720 }
721
722 #[allow(clippy::too_many_arguments)]
723 pub fn update(
724 &mut self,
725 frame: &FrameInfo,
726 gameplay: &GameplayState,
727 ball: &BallFrameState,
728 players: &PlayerFrameState,
729 events: &FrameEventsState,
730 live_play: bool,
731 possession_player_before_sample: Option<&PlayerId>,
732 ) -> SubtrActorResult<()> {
733 self.process_sample(
734 frame,
735 gameplay,
736 ball,
737 players,
738 events,
739 live_play,
740 possession_player_before_sample,
741 )
742 }
743}
744
745fn ball_depth_fractions(level_margin: f32, start_delta: f32, end_delta: f32) -> (f32, f32, f32) {
746 let behind_fraction = interval_fraction_below_threshold(start_delta, end_delta, -level_margin);
747 let level_fraction =
748 interval_fraction_in_scalar_range(start_delta, end_delta, -level_margin, level_margin);
749 let in_front_fraction = (1.0 - behind_fraction - level_fraction).clamp(0.0, 1.0);
750 (behind_fraction, level_fraction, in_front_fraction)
751}
752
753#[cfg(test)]
754#[path = "positioning_tests.rs"]
755mod tests;