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