Skip to main content

subtr_actor/stats/calculators/
fifty_fifty_state.rs

1use super::*;
2
3#[derive(Default)]
4pub struct FiftyFiftyStateCalculator {
5    active_event: Option<ActiveFiftyFifty>,
6    last_resolved_event: Option<FiftyFiftyEvent>,
7    kickoff_touch_window_open: bool,
8}
9
10impl FiftyFiftyStateCalculator {
11    pub fn new() -> Self {
12        Self::default()
13    }
14
15    fn reset(&mut self) {
16        self.active_event = None;
17    }
18
19    fn maybe_resolve_active_event(
20        &mut self,
21        frame: &FrameInfo,
22        ball: &BallFrameState,
23        possession_state: &PossessionState,
24    ) -> Option<FiftyFiftyEvent> {
25        let active = self.active_event.as_ref()?;
26        let age = (frame.time - active.last_touch_time).max(0.0);
27        if age < FIFTY_FIFTY_RESOLUTION_DELAY_SECONDS {
28            return None;
29        }
30
31        let winning_team_is_team_0 = FiftyFiftyCalculator::winning_team_from_ball(active, ball);
32        let possession_team_is_team_0 = possession_state.current_team_is_team_0;
33        let should_resolve = winning_team_is_team_0.is_some()
34            || possession_team_is_team_0.is_some()
35            || age >= FIFTY_FIFTY_MAX_DURATION_SECONDS;
36        if !should_resolve {
37            return None;
38        }
39
40        let active = self.active_event.take()?;
41        let event = FiftyFiftyEvent {
42            start_time: active.start_time,
43            start_frame: active.start_frame,
44            resolve_time: frame.time,
45            resolve_frame: frame.frame_number,
46            is_kickoff: active.is_kickoff,
47            team_zero_player: active.team_zero_player,
48            team_one_player: active.team_one_player,
49            team_zero_position: active.team_zero_position,
50            team_one_position: active.team_one_position,
51            midpoint: active.midpoint,
52            plane_normal: active.plane_normal,
53            winning_team_is_team_0,
54            possession_team_is_team_0,
55        };
56        self.last_resolved_event = Some(event.clone());
57        Some(event)
58    }
59
60    #[allow(clippy::too_many_arguments)]
61    pub fn update(
62        &mut self,
63        frame: &FrameInfo,
64        gameplay: &GameplayState,
65        ball: &BallFrameState,
66        players: &PlayerFrameState,
67        touch_state: &TouchState,
68        possession_state: &PossessionState,
69        live_play_state: &LivePlayState,
70    ) -> FiftyFiftyState {
71        if FiftyFiftyCalculator::kickoff_phase_active(gameplay) {
72            self.kickoff_touch_window_open = true;
73        }
74
75        if !live_play_state.is_live_play {
76            self.reset();
77            return FiftyFiftyState {
78                active_event: None,
79                resolved_events: Vec::new(),
80                last_resolved_event: self.last_resolved_event.clone(),
81            };
82        }
83
84        let has_touch = !touch_state.touch_events.is_empty();
85        let has_contested_touch = touch_state
86            .touch_events
87            .iter()
88            .any(|touch| touch.team_is_team_0)
89            && touch_state
90                .touch_events
91                .iter()
92                .any(|touch| !touch.team_is_team_0);
93
94        if let Some(active_event) = self.active_event.as_mut() {
95            let age = (frame.time - active_event.last_touch_time).max(0.0);
96            if age <= FIFTY_FIFTY_CONTINUATION_TOUCH_WINDOW_SECONDS
97                && active_event.contains_team_touch(&touch_state.touch_events)
98            {
99                active_event.last_touch_time = frame.time;
100                active_event.last_touch_frame = frame.frame_number;
101            }
102        }
103
104        let mut resolved_events = Vec::new();
105        if let Some(event) = self.maybe_resolve_active_event(frame, ball, possession_state) {
106            resolved_events.push(event);
107        }
108
109        if has_contested_touch {
110            if self.active_event.is_none() {
111                self.active_event = FiftyFiftyCalculator::contested_touch(
112                    frame,
113                    players,
114                    &touch_state.touch_events,
115                    self.kickoff_touch_window_open,
116                );
117            }
118        } else if has_touch {
119            if let Some(active_event) = self.active_event.as_mut() {
120                let age = (frame.time - active_event.last_touch_time).max(0.0);
121                if age <= FIFTY_FIFTY_CONTINUATION_TOUCH_WINDOW_SECONDS
122                    && active_event.contains_team_touch(&touch_state.touch_events)
123                {
124                    active_event.last_touch_time = frame.time;
125                    active_event.last_touch_frame = frame.frame_number;
126                }
127            }
128        }
129
130        if has_touch {
131            self.kickoff_touch_window_open = false;
132        }
133
134        FiftyFiftyState {
135            active_event: self.active_event.clone(),
136            resolved_events,
137            last_resolved_event: self.last_resolved_event.clone(),
138        }
139    }
140}