Skip to main content

subtr_actor/stats/calculators/
live_play.rs

1use super::{FrameEventsState, GameplayState};
2use serde::{Deserialize, Serialize};
3
4/// The current phase of gameplay (kickoff, active, replay, etc.).
5#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS)]
6#[serde(rename_all = "snake_case")]
7#[ts(export)]
8pub enum GameplayPhase {
9    #[default]
10    Unknown,
11    KickoffCountdown,
12    KickoffWaitingForTouch,
13    ActivePlay,
14    PostGoal,
15}
16
17impl GameplayPhase {
18    pub fn is_live_play(self) -> bool {
19        matches!(self, Self::ActivePlay)
20    }
21
22    pub fn counts_toward_player_motion(self) -> bool {
23        matches!(self, Self::ActivePlay | Self::KickoffWaitingForTouch)
24    }
25
26    pub fn counts_toward_ball_position_stats(self) -> bool {
27        self.is_live_play()
28    }
29}
30
31/// Shared state describing whether the current frame is live play.
32#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
33pub struct LivePlayState {
34    pub gameplay_phase: GameplayPhase,
35    pub is_live_play: bool,
36}
37
38impl LivePlayState {
39    pub fn new(gameplay_phase: GameplayPhase) -> Self {
40        Self {
41            gameplay_phase,
42            is_live_play: gameplay_phase.is_live_play(),
43        }
44    }
45
46    pub fn active_play() -> Self {
47        Self::new(GameplayPhase::ActivePlay)
48    }
49
50    pub fn counts_toward_player_motion(&self) -> bool {
51        self.gameplay_phase.counts_toward_player_motion()
52    }
53
54    pub fn counts_toward_ball_position_stats(&self) -> bool {
55        self.gameplay_phase.counts_toward_ball_position_stats()
56    }
57}
58
59/// Determines whether each frame is live play and its gameplay phase.
60#[derive(Debug, Clone, Default, PartialEq)]
61pub struct LivePlayTracker {
62    post_goal_phase_active: bool,
63    last_score: Option<(i32, i32)>,
64}
65
66impl LivePlayTracker {
67    fn gameplay_phase_internal(
68        &mut self,
69        gameplay: &GameplayState,
70        events: &FrameEventsState,
71    ) -> GameplayPhase {
72        let kickoff_phase_active = gameplay.kickoff_phase_active();
73        let score_changed = gameplay.current_score().zip(self.last_score).is_some_and(
74            |((team_zero_score, team_one_score), (last_team_zero, last_team_one))| {
75                team_zero_score > last_team_zero || team_one_score > last_team_one
76            },
77        );
78
79        if !events.goal_events.is_empty() || score_changed {
80            self.post_goal_phase_active = true;
81        }
82
83        if kickoff_phase_active {
84            self.post_goal_phase_active = false;
85        }
86
87        if let Some(score) = gameplay.current_score() {
88            self.last_score = Some(score);
89        }
90
91        if gameplay.kickoff_countdown_active() {
92            GameplayPhase::KickoffCountdown
93        } else if gameplay.game_state
94            == Some(crate::stats::calculators::GAME_STATE_GOAL_SCORED_REPLAY)
95            || self.post_goal_phase_active
96        {
97            GameplayPhase::PostGoal
98        } else if gameplay.ball_has_been_hit == Some(false) {
99            GameplayPhase::KickoffWaitingForTouch
100        } else if gameplay.is_live_play() {
101            GameplayPhase::ActivePlay
102        } else {
103            GameplayPhase::Unknown
104        }
105    }
106
107    pub fn state_parts(
108        &mut self,
109        gameplay: &GameplayState,
110        events: &FrameEventsState,
111    ) -> LivePlayState {
112        let gameplay_phase = self.gameplay_phase_internal(gameplay, events);
113        LivePlayState::new(gameplay_phase)
114    }
115}
116
117#[cfg(test)]
118#[path = "live_play_tests.rs"]
119mod tests;