subtr_actor/stats/calculators/
live_play.rs1use super::{FrameEventsState, GameplayState};
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS)]
5#[serde(rename_all = "snake_case")]
6#[ts(export)]
7pub enum GameplayPhase {
8 #[default]
9 Unknown,
10 KickoffCountdown,
11 KickoffWaitingForTouch,
12 ActivePlay,
13 PostGoal,
14}
15
16impl GameplayPhase {
17 pub fn is_live_play(self) -> bool {
18 matches!(self, Self::ActivePlay)
19 }
20
21 pub fn counts_toward_player_motion(self) -> bool {
22 matches!(self, Self::ActivePlay | Self::KickoffWaitingForTouch)
23 }
24
25 pub fn counts_toward_ball_position_stats(self) -> bool {
26 self.is_live_play()
27 }
28}
29
30#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
31pub struct LivePlayState {
32 pub gameplay_phase: GameplayPhase,
33 pub is_live_play: bool,
34}
35
36impl LivePlayState {
37 pub fn counts_toward_player_motion(&self) -> bool {
38 self.gameplay_phase.counts_toward_player_motion()
39 }
40
41 pub fn counts_toward_ball_position_stats(&self) -> bool {
42 self.gameplay_phase.counts_toward_ball_position_stats()
43 }
44}
45
46#[derive(Debug, Clone, Default, PartialEq)]
47pub struct LivePlayTracker {
48 post_goal_phase_active: bool,
49 last_score: Option<(i32, i32)>,
50}
51
52impl LivePlayTracker {
53 fn gameplay_phase_internal(
54 &mut self,
55 gameplay: &GameplayState,
56 events: &FrameEventsState,
57 ) -> GameplayPhase {
58 let kickoff_phase_active = gameplay.kickoff_phase_active();
59 let score_changed = gameplay.current_score().zip(self.last_score).is_some_and(
60 |((team_zero_score, team_one_score), (last_team_zero, last_team_one))| {
61 team_zero_score > last_team_zero || team_one_score > last_team_one
62 },
63 );
64
65 if !events.goal_events.is_empty() || score_changed {
66 self.post_goal_phase_active = true;
67 }
68
69 if kickoff_phase_active {
70 self.post_goal_phase_active = false;
71 }
72
73 if let Some(score) = gameplay.current_score() {
74 self.last_score = Some(score);
75 }
76
77 if gameplay.game_state == Some(crate::stats::calculators::GAME_STATE_KICKOFF_COUNTDOWN)
78 || gameplay.kickoff_countdown_time.is_some_and(|time| time > 0)
79 {
80 GameplayPhase::KickoffCountdown
81 } else if gameplay.game_state
82 == Some(crate::stats::calculators::GAME_STATE_GOAL_SCORED_REPLAY)
83 || self.post_goal_phase_active
84 {
85 GameplayPhase::PostGoal
86 } else if gameplay.ball_has_been_hit == Some(false) {
87 GameplayPhase::KickoffWaitingForTouch
88 } else if gameplay.is_live_play() {
89 GameplayPhase::ActivePlay
90 } else {
91 GameplayPhase::Unknown
92 }
93 }
94
95 pub fn state_parts(
96 &mut self,
97 gameplay: &GameplayState,
98 events: &FrameEventsState,
99 ) -> LivePlayState {
100 let gameplay_phase = self.gameplay_phase_internal(gameplay, events);
101 LivePlayState {
102 gameplay_phase,
103 is_live_play: gameplay_phase.is_live_play(),
104 }
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111 use crate::GoalEvent;
112
113 #[test]
114 fn kickoff_waiting_for_first_touch_is_not_live_play() {
115 let mut tracker = LivePlayTracker::default();
116 let gameplay = GameplayState {
117 ball_has_been_hit: Some(false),
118 ..Default::default()
119 };
120 let state = tracker.state_parts(&gameplay, &FrameEventsState::default());
121
122 assert_eq!(state.gameplay_phase, GameplayPhase::KickoffWaitingForTouch);
123 assert!(!state.is_live_play);
124 assert!(state.gameplay_phase.counts_toward_player_motion());
125 }
126
127 #[test]
128 fn goal_event_enters_post_goal_phase() {
129 let mut tracker = LivePlayTracker::default();
130 let gameplay = GameplayState::default();
131 let events = FrameEventsState {
132 goal_events: vec![GoalEvent {
133 time: 10.0,
134 frame: 1,
135 scoring_team_is_team_0: true,
136 player: None,
137 team_zero_score: None,
138 team_one_score: None,
139 }],
140 ..Default::default()
141 };
142
143 let state = tracker.state_parts(&gameplay, &events);
144
145 assert_eq!(state.gameplay_phase, GameplayPhase::PostGoal);
146 assert!(!state.is_live_play);
147 }
148}