Skip to main content

subtr_actor/stats/calculators/
live_play.rs

1use 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)]
109#[path = "live_play_tests.rs"]
110mod tests;