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_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}