subtr_actor/stats/calculators/
backboard_bounce.rs1use 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}