subtr-actor 1.0.0

Rocket League replay transformer
Documentation
use super::*;

const CENTER_MAX_DURATION_SECONDS: f32 = 3.0;
const CENTER_MIN_BALL_TRAVEL_DISTANCE: f32 = 500.0;
const CENTER_MIN_LATERAL_DISTANCE: f32 = 500.0;
const CENTER_MIN_START_ABS_X: f32 = 1600.0;
const CENTER_MAX_END_ABS_X: f32 = 1400.0;
const CENTER_MIN_START_ATTACKING_Y: f32 = BOOST_PAD_MIDFIELD_TOLERANCE_Y;
const CENTER_MIN_END_ATTACKING_Y: f32 = FIELD_ZONE_BOUNDARY_Y;

#[derive(Debug, Clone, PartialEq, Serialize, ts_rs::TS)]
#[ts(export)]
pub struct CenterEvent {
    pub time: f32,
    pub frame: usize,
    #[ts(as = "crate::interop::ts_bindings::RemoteIdTs")]
    pub player: PlayerId,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub player_position: Option<[f32; 3]>,
    pub is_team_0: bool,
    pub start_time: f32,
    pub start_frame: usize,
    pub duration: f32,
    pub start_ball_position: [f32; 3],
    pub end_ball_position: [f32; 3],
    pub ball_travel_distance: f32,
    pub ball_advance_distance: f32,
    pub lateral_centering_distance: f32,
}

#[derive(Debug, Clone)]
struct PendingCenterTouch {
    player: PlayerId,
    player_position: Option<[f32; 3]>,
    is_team_0: bool,
    time: f32,
    frame: usize,
    ball_position: glam::Vec3,
}

#[derive(Debug, Clone, Default)]
pub struct CenterCalculator {
    events: EventStream<CenterEvent>,
    pending_touch: Option<PendingCenterTouch>,
}

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

    pub fn events(&self) -> &[CenterEvent] {
        self.events.all()
    }

    pub fn new_events(&self) -> &[CenterEvent] {
        self.events.new_events()
    }

    fn center_event_for_position(
        pending: &PendingCenterTouch,
        frame: &FrameInfo,
        ball_position: glam::Vec3,
    ) -> Option<CenterEvent> {
        let duration = frame.time - pending.time;
        if !(0.0..=CENTER_MAX_DURATION_SECONDS).contains(&duration) {
            return None;
        }

        let start_normalized_y = normalized_y(pending.is_team_0, pending.ball_position);
        let end_normalized_y = normalized_y(pending.is_team_0, ball_position);
        if start_normalized_y < CENTER_MIN_START_ATTACKING_Y
            || end_normalized_y < CENTER_MIN_END_ATTACKING_Y
        {
            return None;
        }

        let start_abs_x = pending.ball_position.x.abs();
        let end_abs_x = ball_position.x.abs();
        let lateral_centering_distance = start_abs_x - end_abs_x;
        if start_abs_x < CENTER_MIN_START_ABS_X
            || end_abs_x > CENTER_MAX_END_ABS_X
            || lateral_centering_distance < CENTER_MIN_LATERAL_DISTANCE
        {
            return None;
        }

        let ball_delta = ball_position - pending.ball_position;
        let ball_travel_distance = ball_delta.length();
        if ball_travel_distance < CENTER_MIN_BALL_TRAVEL_DISTANCE {
            return None;
        }

        let team_forward_sign = if pending.is_team_0 { 1.0 } else { -1.0 };
        Some(CenterEvent {
            time: frame.time,
            frame: frame.frame_number,
            player: pending.player.clone(),
            player_position: pending.player_position,
            is_team_0: pending.is_team_0,
            start_time: pending.time,
            start_frame: pending.frame,
            duration,
            start_ball_position: pending.ball_position.to_array(),
            end_ball_position: ball_position.to_array(),
            ball_travel_distance,
            ball_advance_distance: ball_delta.y * team_forward_sign,
            lateral_centering_distance,
        })
    }

    fn record_center(&mut self, _frame: &FrameInfo, event: CenterEvent) {
        self.events.push(event);
        self.pending_touch = None;
    }

    fn update_pending_center(&mut self, frame: &FrameInfo, ball_position: glam::Vec3) {
        let Some(pending) = self.pending_touch.as_ref() else {
            return;
        };
        let duration = frame.time - pending.time;
        if duration > CENTER_MAX_DURATION_SECONDS {
            self.pending_touch = None;
            return;
        }

        if let Some(event) = Self::center_event_for_position(pending, frame, ball_position) {
            self.record_center(frame, event);
        }
    }

    fn player_has_disqualifying_event(
        events: &FrameEventsState,
        player: &PlayerId,
        is_team_0: bool,
    ) -> bool {
        events.player_stat_events.iter().any(|event| {
            event.kind == PlayerStatEventKind::Shot
                && event.player == *player
                && event.is_team_0 == is_team_0
        }) || events
            .goal_events
            .iter()
            .any(|event| match event.player.as_ref() {
                Some(scorer) => scorer == player,
                None => event.scoring_team_is_team_0 == is_team_0,
            })
    }

    fn clear_disqualified_pending_center(&mut self, events: &FrameEventsState) {
        let should_clear = self.pending_touch.as_ref().is_some_and(|pending| {
            Self::player_has_disqualifying_event(events, &pending.player, pending.is_team_0)
        });
        if should_clear {
            self.pending_touch = None;
        }
    }

    pub fn update(
        &mut self,
        frame: &FrameInfo,
        ball: &BallFrameState,
        touch_state: &TouchState,
        frame_events: &FrameEventsState,
        live_play_state: &LivePlayState,
    ) -> SubtrActorResult<()> {
        self.events.begin_update();
        if !live_play_state.is_live_play {
            self.pending_touch = None;
            return Ok(());
        }

        let Some(ball_position) = ball.position() else {
            return Ok(());
        };

        self.clear_disqualified_pending_center(frame_events);
        self.update_pending_center(frame, ball_position);

        for touch in sequential_touch_events(&touch_state.touch_events) {
            let Some(player) = touch.player.clone() else {
                self.pending_touch = None;
                continue;
            };

            if Self::player_has_disqualifying_event(frame_events, &player, touch.team_is_team_0) {
                self.pending_touch = None;
                continue;
            }

            self.pending_touch = Some(PendingCenterTouch {
                player,
                player_position: touch
                    .player_position
                    .map(|position| vec_to_glam(&position).to_array()),
                is_team_0: touch.team_is_team_0,
                time: touch.time,
                frame: touch.frame,
                ball_position,
            });
        }

        Ok(())
    }
}

#[cfg(test)]
#[path = "center_tests.rs"]
mod tests;