subtr-actor 0.4.0

Rocket League replay transformer
Documentation
use super::*;

#[derive(Debug, Clone, PartialEq, Serialize, ts_rs::TS)]
#[ts(export)]
pub struct BackboardBounceEvent {
    pub time: f32,
    pub frame: usize,
    #[ts(as = "crate::ts_bindings::RemoteIdTs")]
    pub player: PlayerId,
    pub is_team_0: bool,
}

#[derive(Debug, Clone, Default, PartialEq)]
pub struct BackboardBounceState {
    pub bounce_events: Vec<BackboardBounceEvent>,
    pub last_bounce_event: Option<BackboardBounceEvent>,
}

#[derive(Default)]
pub struct BackboardBounceCalculator {
    previous_ball_velocity: Option<glam::Vec3>,
    last_touch: Option<TouchEvent>,
    last_bounce_event: Option<BackboardBounceEvent>,
}

impl BackboardBounceCalculator {
    pub fn new() -> Self {
        Self::default()
    }

    fn detect_bounce(
        &self,
        frame: &FrameInfo,
        ball: Option<&BallSample>,
        touch_events: &[TouchEvent],
    ) -> Option<BackboardBounceEvent> {
        const BACKBOARD_MIN_BALL_Z: f32 = 500.0;
        const BACKBOARD_MIN_NORMALIZED_Y: f32 = 4700.0;
        const BACKBOARD_MAX_ABS_X: f32 = 1600.0;
        const BACKBOARD_MIN_APPROACH_SPEED_Y: f32 = 350.0;
        const BACKBOARD_MIN_REBOUND_SPEED_Y: f32 = 250.0;
        const BACKBOARD_TOUCH_ATTRIBUTION_MAX_SECONDS: f32 = 2.5;

        if !touch_events.is_empty() {
            return None;
        }

        let last_touch = self.last_touch.as_ref()?;
        let player = last_touch.player.clone()?;
        let current_ball = ball?;
        let previous_ball_velocity = self.previous_ball_velocity?;

        if (frame.time - last_touch.time).max(0.0) > BACKBOARD_TOUCH_ATTRIBUTION_MAX_SECONDS {
            return None;
        }

        let ball_position = current_ball.position();
        if ball_position.x.abs() > BACKBOARD_MAX_ABS_X || ball_position.z < BACKBOARD_MIN_BALL_Z {
            return None;
        }

        let normalized_position_y = normalized_y(last_touch.team_is_team_0, ball_position);
        if normalized_position_y < BACKBOARD_MIN_NORMALIZED_Y {
            return None;
        }

        let previous_normalized_velocity_y = if last_touch.team_is_team_0 {
            previous_ball_velocity.y
        } else {
            -previous_ball_velocity.y
        };
        let current_normalized_velocity_y = if last_touch.team_is_team_0 {
            current_ball.velocity().y
        } else {
            -current_ball.velocity().y
        };

        if previous_normalized_velocity_y < BACKBOARD_MIN_APPROACH_SPEED_Y {
            return None;
        }
        if current_normalized_velocity_y > -BACKBOARD_MIN_REBOUND_SPEED_Y {
            return None;
        }

        Some(BackboardBounceEvent {
            time: frame.time,
            frame: frame.frame_number,
            player,
            is_team_0: last_touch.team_is_team_0,
        })
    }

    pub fn update(
        &mut self,
        frame: &FrameInfo,
        ball: &BallFrameState,
        events: &FrameEventsState,
        live_play_state: &LivePlayState,
    ) -> BackboardBounceState {
        if !live_play_state.is_live_play {
            self.previous_ball_velocity = ball.velocity();
            self.last_touch = None;
            self.last_bounce_event = None;
            return BackboardBounceState::default();
        }

        let bounce_events: Vec<_> = self
            .detect_bounce(frame, ball.sample(), &events.touch_events)
            .into_iter()
            .collect();
        if let Some(last_bounce_event) = bounce_events.last() {
            self.last_bounce_event = Some(last_bounce_event.clone());
        }

        if let Some(last_touch) = events.touch_events.last() {
            self.last_touch = Some(last_touch.clone());
        }
        self.previous_ball_velocity = ball.velocity();

        BackboardBounceState {
            bounce_events,
            last_bounce_event: self.last_bounce_event.clone(),
        }
    }
}