Skip to main content

subtr_actor/stats/calculators/
one_timer.rs

1use super::*;
2
3const ONE_TIMER_MIN_BALL_SPEED: f32 = 1000.0;
4const ONE_TIMER_MIN_GOAL_ALIGNMENT_COSINE: f32 = 0.65;
5const GOAL_CENTER_Y: f32 = 5120.0;
6
7#[derive(Debug, Clone, PartialEq, Serialize, ts_rs::TS)]
8#[ts(export)]
9pub struct OneTimerEvent {
10    pub time: f32,
11    pub frame: usize,
12    #[ts(as = "crate::ts_bindings::RemoteIdTs")]
13    pub player: PlayerId,
14    #[ts(as = "crate::ts_bindings::RemoteIdTs")]
15    pub passer: PlayerId,
16    pub is_team_0: bool,
17    pub pass_start_time: f32,
18    pub pass_start_frame: usize,
19    pub pass_duration: f32,
20    pub pass_travel_distance: f32,
21    pub pass_advance_distance: f32,
22    pub ball_speed: f32,
23    pub goal_alignment: f32,
24}
25
26#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, ts_rs::TS)]
27#[ts(export)]
28pub struct OneTimerPlayerStats {
29    pub count: u32,
30    pub total_ball_speed: f32,
31    pub fastest_ball_speed: f32,
32    pub total_pass_distance: f32,
33    pub is_last_one_timer: bool,
34    pub last_one_timer_time: Option<f32>,
35    pub last_one_timer_frame: Option<usize>,
36    pub time_since_last_one_timer: Option<f32>,
37    pub frames_since_last_one_timer: Option<usize>,
38}
39
40impl OneTimerPlayerStats {
41    pub fn average_ball_speed(&self) -> f32 {
42        if self.count == 0 {
43            0.0
44        } else {
45            self.total_ball_speed / self.count as f32
46        }
47    }
48
49    pub fn average_pass_distance(&self) -> f32 {
50        if self.count == 0 {
51            0.0
52        } else {
53            self.total_pass_distance / self.count as f32
54        }
55    }
56}
57
58#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, ts_rs::TS)]
59#[ts(export)]
60pub struct OneTimerTeamStats {
61    pub count: u32,
62    pub total_ball_speed: f32,
63    pub fastest_ball_speed: f32,
64}
65
66impl OneTimerTeamStats {
67    pub fn average_ball_speed(&self) -> f32 {
68        if self.count == 0 {
69            0.0
70        } else {
71            self.total_ball_speed / self.count as f32
72        }
73    }
74}
75
76#[derive(Debug, Clone, Default)]
77pub struct OneTimerCalculator {
78    player_stats: HashMap<PlayerId, OneTimerPlayerStats>,
79    team_zero_stats: OneTimerTeamStats,
80    team_one_stats: OneTimerTeamStats,
81    events: Vec<OneTimerEvent>,
82    processed_pass_events: usize,
83    current_last_one_timer_player: Option<PlayerId>,
84}
85
86impl OneTimerCalculator {
87    pub fn new() -> Self {
88        Self::default()
89    }
90
91    pub fn player_stats(&self) -> &HashMap<PlayerId, OneTimerPlayerStats> {
92        &self.player_stats
93    }
94
95    pub fn team_zero_stats(&self) -> &OneTimerTeamStats {
96        &self.team_zero_stats
97    }
98
99    pub fn team_one_stats(&self) -> &OneTimerTeamStats {
100        &self.team_one_stats
101    }
102
103    pub fn events(&self) -> &[OneTimerEvent] {
104        &self.events
105    }
106
107    fn begin_sample(&mut self, frame: &FrameInfo) {
108        for stats in self.player_stats.values_mut() {
109            stats.is_last_one_timer = false;
110            stats.time_since_last_one_timer = stats
111                .last_one_timer_time
112                .map(|time| (frame.time - time).max(0.0));
113            stats.frames_since_last_one_timer = stats
114                .last_one_timer_frame
115                .map(|last_frame| frame.frame_number.saturating_sub(last_frame));
116        }
117    }
118
119    fn one_timer_event_for_pass(pass: &PassEvent, ball: &BallFrameState) -> Option<OneTimerEvent> {
120        let ball = ball.sample()?;
121        let ball_position = ball.position();
122        let ball_velocity = ball.velocity();
123        let ball_speed = ball_velocity.length();
124        if ball_speed < ONE_TIMER_MIN_BALL_SPEED {
125            return None;
126        }
127
128        let target_y = if pass.is_team_0 {
129            GOAL_CENTER_Y
130        } else {
131            -GOAL_CENTER_Y
132        };
133        let goal_direction = glam::Vec3::new(0.0, target_y, ball_position.z) - ball_position;
134        let goal_alignment = goal_direction
135            .normalize_or_zero()
136            .dot(ball_velocity.normalize_or_zero());
137        if goal_alignment < ONE_TIMER_MIN_GOAL_ALIGNMENT_COSINE {
138            return None;
139        }
140
141        Some(OneTimerEvent {
142            time: pass.time,
143            frame: pass.frame,
144            player: pass.receiver.clone(),
145            passer: pass.passer.clone(),
146            is_team_0: pass.is_team_0,
147            pass_start_time: pass.start_time,
148            pass_start_frame: pass.start_frame,
149            pass_duration: pass.duration,
150            pass_travel_distance: pass.ball_travel_distance,
151            pass_advance_distance: pass.ball_advance_distance,
152            ball_speed,
153            goal_alignment,
154        })
155    }
156
157    fn record_one_timer(&mut self, frame: &FrameInfo, event: OneTimerEvent) {
158        let player_stats = self.player_stats.entry(event.player.clone()).or_default();
159        player_stats.count += 1;
160        player_stats.total_ball_speed += event.ball_speed;
161        player_stats.fastest_ball_speed = player_stats.fastest_ball_speed.max(event.ball_speed);
162        player_stats.total_pass_distance += event.pass_travel_distance;
163        player_stats.last_one_timer_time = Some(event.time);
164        player_stats.last_one_timer_frame = Some(event.frame);
165        player_stats.time_since_last_one_timer = Some((frame.time - event.time).max(0.0));
166        player_stats.frames_since_last_one_timer =
167            Some(frame.frame_number.saturating_sub(event.frame));
168
169        let team_stats = if event.is_team_0 {
170            &mut self.team_zero_stats
171        } else {
172            &mut self.team_one_stats
173        };
174        team_stats.count += 1;
175        team_stats.total_ball_speed += event.ball_speed;
176        team_stats.fastest_ball_speed = team_stats.fastest_ball_speed.max(event.ball_speed);
177
178        self.current_last_one_timer_player = Some(event.player.clone());
179        self.events.push(event);
180    }
181
182    pub fn update(
183        &mut self,
184        frame: &FrameInfo,
185        ball: &BallFrameState,
186        pass_calculator: &PassCalculator,
187        live_play: bool,
188    ) -> SubtrActorResult<()> {
189        self.begin_sample(frame);
190        if !live_play {
191            self.current_last_one_timer_player = None;
192            self.processed_pass_events = pass_calculator.events().len();
193            return Ok(());
194        }
195
196        for pass in &pass_calculator.events()[self.processed_pass_events..] {
197            if pass.frame != frame.frame_number {
198                continue;
199            }
200            if let Some(event) = Self::one_timer_event_for_pass(pass, ball) {
201                self.record_one_timer(frame, event);
202            }
203        }
204        self.processed_pass_events = pass_calculator.events().len();
205
206        if let Some(player_id) = self.current_last_one_timer_player.as_ref() {
207            if let Some(stats) = self.player_stats.get_mut(player_id) {
208                stats.is_last_one_timer = true;
209            }
210        }
211
212        Ok(())
213    }
214}
215
216#[cfg(test)]
217#[path = "one_timer_tests.rs"]
218mod tests;