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_touch_time: active.team_zero_touch_time,
50            team_zero_touch_frame: active.team_zero_touch_frame,
51            team_zero_dodge_contact: active.team_zero_dodge_contact,
52            team_one_touch_time: active.team_one_touch_time,
53            team_one_touch_frame: active.team_one_touch_frame,
54            team_one_dodge_contact: active.team_one_dodge_contact,
55            team_zero_position: active.team_zero_position,
56            team_one_position: active.team_one_position,
57            midpoint: active.midpoint,
58            plane_normal: active.plane_normal,
59            winning_team_is_team_0,
60            possession_team_is_team_0,
61        };
62        self.last_resolved_event = Some(event.clone());
63        Some(event)
64    }
65
66    #[allow(clippy::too_many_arguments)]
67    pub fn update(
68        &mut self,
69        frame: &FrameInfo,
70        gameplay: &GameplayState,
71        ball: &BallFrameState,
72        players: &PlayerFrameState,
73        touch_state: &TouchState,
74        possession_state: &PossessionState,
75        live_play_state: &LivePlayState,
76    ) -> FiftyFiftyState {
77        if FiftyFiftyCalculator::kickoff_phase_active(gameplay) {
78            self.kickoff_touch_window_open = true;
79        }
80
81        if !live_play_state.is_live_play {
82            self.reset();
83            return FiftyFiftyState {
84                active_event: None,
85                resolved_events: Vec::new(),
86                last_resolved_event: self.last_resolved_event.clone(),
87            };
88        }
89
90        let has_touch = !touch_state.touch_events.is_empty();
91        let has_contested_touch = touch_state
92            .touch_events
93            .iter()
94            .any(|touch| touch.team_is_team_0)
95            && touch_state
96                .touch_events
97                .iter()
98                .any(|touch| !touch.team_is_team_0);
99
100        if let Some(active_event) = self.active_event.as_mut() {
101            let age = (frame.time - active_event.last_touch_time).max(0.0);
102            if age <= FIFTY_FIFTY_CONTINUATION_TOUCH_WINDOW_SECONDS
103                && active_event.contains_team_touch(&touch_state.touch_events)
104            {
105                active_event.last_touch_time = frame.time;
106                active_event.last_touch_frame = frame.frame_number;
107            }
108        }
109
110        let mut resolved_events = Vec::new();
111        if let Some(event) = self.maybe_resolve_active_event(frame, ball, possession_state) {
112            resolved_events.push(event);
113        }
114
115        if has_contested_touch {
116            if self.active_event.is_none() {
117                self.active_event = FiftyFiftyCalculator::contested_touch(
118                    frame,
119                    players,
120                    &touch_state.touch_events,
121                    self.kickoff_touch_window_open,
122                );
123            }
124        } else if has_touch {
125            if let Some(active_event) = self.active_event.as_mut() {
126                let age = (frame.time - active_event.last_touch_time).max(0.0);
127                if age <= FIFTY_FIFTY_CONTINUATION_TOUCH_WINDOW_SECONDS
128                    && active_event.contains_team_touch(&touch_state.touch_events)
129                {
130                    active_event.last_touch_time = frame.time;
131                    active_event.last_touch_frame = frame.frame_number;
132                }
133            }
134        }
135
136        if has_touch {
137            self.kickoff_touch_window_open = false;
138        }
139
140        FiftyFiftyState {
141            active_event: self.active_event.clone(),
142            resolved_events,
143            last_resolved_event: self.last_resolved_event.clone(),
144        }
145    }
146}