Skip to main content

subtr_actor/stats/calculators/
backboard_bounce.rs

1use super::*;
2
3#[derive(Debug, Clone, PartialEq, Serialize, ts_rs::TS)]
4#[ts(export)]
5pub struct BackboardBounceEvent {
6    pub time: f32,
7    pub frame: usize,
8    #[ts(as = "crate::ts_bindings::RemoteIdTs")]
9    pub player: PlayerId,
10    pub is_team_0: bool,
11}
12
13#[derive(Debug, Clone, Default, PartialEq)]
14pub struct BackboardBounceState {
15    pub bounce_events: Vec<BackboardBounceEvent>,
16    pub last_bounce_event: Option<BackboardBounceEvent>,
17}
18
19#[derive(Default)]
20pub struct BackboardBounceCalculator {
21    previous_ball_velocity: Option<glam::Vec3>,
22    last_touch: Option<TouchEvent>,
23    last_bounce_event: Option<BackboardBounceEvent>,
24}
25
26impl BackboardBounceCalculator {
27    pub fn new() -> Self {
28        Self::default()
29    }
30
31    fn detect_bounce(
32        &self,
33        frame: &FrameInfo,
34        ball: Option<&BallSample>,
35        touch_events: &[TouchEvent],
36    ) -> Option<BackboardBounceEvent> {
37        const BACKBOARD_MIN_BALL_Z: f32 = 500.0;
38        const BACKBOARD_MIN_NORMALIZED_Y: f32 = 4700.0;
39        const BACKBOARD_MAX_ABS_X: f32 = 1600.0;
40        const BACKBOARD_MIN_APPROACH_SPEED_Y: f32 = 350.0;
41        const BACKBOARD_MIN_REBOUND_SPEED_Y: f32 = 250.0;
42        const BACKBOARD_TOUCH_ATTRIBUTION_MAX_SECONDS: f32 = 2.5;
43
44        if !touch_events.is_empty() {
45            return None;
46        }
47
48        let last_touch = self.last_touch.as_ref()?;
49        let player = last_touch.player.clone()?;
50        let current_ball = ball?;
51        let previous_ball_velocity = self.previous_ball_velocity?;
52
53        if (frame.time - last_touch.time).max(0.0) > BACKBOARD_TOUCH_ATTRIBUTION_MAX_SECONDS {
54            return None;
55        }
56
57        let ball_position = current_ball.position();
58        if ball_position.x.abs() > BACKBOARD_MAX_ABS_X || ball_position.z < BACKBOARD_MIN_BALL_Z {
59            return None;
60        }
61
62        let normalized_position_y = normalized_y(last_touch.team_is_team_0, ball_position);
63        if normalized_position_y < BACKBOARD_MIN_NORMALIZED_Y {
64            return None;
65        }
66
67        let previous_normalized_velocity_y = if last_touch.team_is_team_0 {
68            previous_ball_velocity.y
69        } else {
70            -previous_ball_velocity.y
71        };
72        let current_normalized_velocity_y = if last_touch.team_is_team_0 {
73            current_ball.velocity().y
74        } else {
75            -current_ball.velocity().y
76        };
77
78        if previous_normalized_velocity_y < BACKBOARD_MIN_APPROACH_SPEED_Y {
79            return None;
80        }
81        if current_normalized_velocity_y > -BACKBOARD_MIN_REBOUND_SPEED_Y {
82            return None;
83        }
84
85        Some(BackboardBounceEvent {
86            time: frame.time,
87            frame: frame.frame_number,
88            player,
89            is_team_0: last_touch.team_is_team_0,
90        })
91    }
92
93    pub fn update(
94        &mut self,
95        frame: &FrameInfo,
96        ball: &BallFrameState,
97        events: &FrameEventsState,
98        live_play_state: &LivePlayState,
99    ) -> BackboardBounceState {
100        if !live_play_state.is_live_play {
101            self.previous_ball_velocity = ball.velocity();
102            self.last_touch = None;
103            self.last_bounce_event = None;
104            return BackboardBounceState::default();
105        }
106
107        let bounce_events: Vec<_> = self
108            .detect_bounce(frame, ball.sample(), &events.touch_events)
109            .into_iter()
110            .collect();
111        if let Some(last_bounce_event) = bounce_events.last() {
112            self.last_bounce_event = Some(last_bounce_event.clone());
113        }
114
115        if let Some(last_touch) = events.touch_events.last() {
116            self.last_touch = Some(last_touch.clone());
117        }
118        self.previous_ball_velocity = ball.velocity();
119
120        BackboardBounceState {
121            bounce_events,
122            last_bounce_event: self.last_bounce_event.clone(),
123        }
124    }
125}