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)]
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#[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#[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;