Skip to main content

subtr_actor/stats/calculators/
frame_components.rs

1use crate::*;
2
3use super::{BallSample, DemoEventSample, PlayerSample};
4
5/// Per-frame timing and index metadata.
6#[derive(Debug, Clone, Default)]
7pub struct FrameInfo {
8    pub frame_number: usize,
9    pub time: f32,
10    pub dt: f32,
11    pub seconds_remaining: Option<i32>,
12}
13
14/// Per-frame gameplay state such as game phase and ball-hit flags.
15#[derive(Debug, Clone, Default)]
16pub struct GameplayState {
17    pub game_state: Option<i32>,
18    pub ball_has_been_hit: Option<bool>,
19    pub kickoff_countdown_time: Option<i32>,
20    pub team_zero_score: Option<i32>,
21    pub team_one_score: Option<i32>,
22    pub possession_team_is_team_0: Option<bool>,
23    pub scored_on_team_is_team_0: Option<bool>,
24    pub current_in_game_team_player_counts: [usize; 2],
25}
26
27impl GameplayState {
28    pub fn is_live_play(&self) -> bool {
29        !self.kickoff_phase_active() && self.game_state != Some(GAME_STATE_GOAL_SCORED_REPLAY)
30    }
31
32    pub fn current_score(&self) -> Option<(i32, i32)> {
33        Some((self.team_zero_score?, self.team_one_score?))
34    }
35
36    pub fn kickoff_countdown_active(&self) -> bool {
37        self.kickoff_countdown_time
38            .is_some_and(|time| (1..=3).contains(&time))
39            || (self.game_state == Some(GAME_STATE_KICKOFF_COUNTDOWN)
40                && self.kickoff_countdown_time.is_none())
41    }
42
43    pub fn kickoff_phase_active(&self) -> bool {
44        self.game_state != Some(GAME_STATE_GOAL_SCORED_REPLAY)
45            && (self.kickoff_countdown_active() || self.ball_has_been_hit == Some(false))
46    }
47
48    pub fn current_in_game_team_player_count(&self, is_team_0: bool) -> usize {
49        self.current_in_game_team_player_counts[usize::from(!is_team_0)]
50    }
51}
52
53/// Per-frame ball state (present with position/velocity, or absent).
54#[derive(Debug, Clone, Default)]
55pub enum BallFrameState {
56    #[default]
57    Missing,
58    Present(BallSample),
59}
60
61impl BallFrameState {
62    pub fn sample(&self) -> Option<&BallSample> {
63        match self {
64            Self::Missing => None,
65            Self::Present(ball) => Some(ball),
66        }
67    }
68
69    pub fn into_sample(self) -> Option<BallSample> {
70        match self {
71            Self::Missing => None,
72            Self::Present(ball) => Some(ball),
73        }
74    }
75
76    pub fn position(&self) -> Option<glam::Vec3> {
77        self.sample().map(BallSample::position)
78    }
79
80    pub fn velocity(&self) -> Option<glam::Vec3> {
81        self.sample().map(BallSample::velocity)
82    }
83}
84
85impl From<BallSample> for BallFrameState {
86    fn from(ball: BallSample) -> Self {
87        Self::Present(ball)
88    }
89}
90
91impl From<Option<BallSample>> for BallFrameState {
92    fn from(ball: Option<BallSample>) -> Self {
93        match ball {
94            Some(ball) => Self::Present(ball),
95            None => Self::Missing,
96        }
97    }
98}
99
100/// Per-frame per-player state (position, velocity, boost, inputs).
101#[derive(Debug, Clone, Default)]
102pub struct PlayerFrameState {
103    pub players: Vec<PlayerSample>,
104}
105
106impl PlayerFrameState {
107    pub fn player(&self, player_id: &PlayerId) -> Option<&PlayerSample> {
108        self.players
109            .iter()
110            .find(|player| &player.player_id == player_id)
111    }
112
113    pub fn player_position(&self, player_id: &PlayerId) -> Option<[f32; 3]> {
114        self.player(player_id)
115            .and_then(PlayerSample::position)
116            .map(|position| position.to_array())
117    }
118}
119
120/// Per-frame discrete game events extracted from the replay.
121#[derive(Debug, Clone, Default)]
122pub struct FrameEventsState {
123    pub active_demos: Vec<DemoEventSample>,
124    pub demo_events: Vec<DemolishInfo>,
125    pub boost_pad_events: Vec<BoostPadEvent>,
126    pub touch_events: Vec<TouchEvent>,
127    /// Whether the replay exposes the authoritative refreshed-dodge counter.
128    ///
129    /// When this is present, flip-reset detection should prefer counter-derived
130    /// dodge refreshes and avoid geometry fallback candidates.
131    pub dodge_refreshed_counter_available: bool,
132    pub dodge_refreshed_events: Vec<DodgeRefreshedEvent>,
133    pub player_stat_events: Vec<PlayerStatEvent>,
134    pub goal_events: Vec<GoalEvent>,
135}
136
137pub(crate) const GAME_STATE_KICKOFF_COUNTDOWN: i32 = 53;
138pub(crate) const GAME_STATE_GOAL_SCORED_REPLAY: i32 = 67;