subtr_actor/stats/calculators/
fifty_fifty_state.rs1use 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}