subtr-actor 0.11.0

Rocket League replay transformer
Documentation
use super::*;

const FLIP_RESET_MIN_DODGE_TOUCH_DELAY_SECONDS: f32 = 0.05;
const FLIP_RESET_MAX_DODGE_TOUCH_DELAY_SECONDS: f32 = 2.0;
const FLIP_RESET_GROUNDED_Z: f32 = 80.0;

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

#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct ConfirmedFlipResetEvent {
    pub time: f32,
    pub frame: usize,
    pub reset_time: f32,
    pub reset_frame: usize,
    pub player: PlayerId,
    pub is_team_0: bool,
    pub counter_value: i32,
    pub time_since_reset: f32,
}

#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, ts_rs::TS)]
#[ts(export)]
pub struct DodgeResetStats {
    pub count: u32,
    pub on_ball_count: u32,
}

#[derive(Debug, Clone, Default, PartialEq)]
pub struct DodgeResetCalculator {
    player_stats: HashMap<PlayerId, DodgeResetStats>,
    events: Vec<DodgeResetEvent>,
    on_ball_events: Vec<DodgeRefreshedEvent>,
    confirmed_flip_reset_events: Vec<ConfirmedFlipResetEvent>,
    pending_on_ball_resets: HashMap<PlayerId, DodgeRefreshedEvent>,
    pending_reset_dodge_started: HashSet<PlayerId>,
    previous_dodge_active: HashMap<PlayerId, bool>,
}

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

    pub fn player_stats(&self) -> &HashMap<PlayerId, DodgeResetStats> {
        &self.player_stats
    }

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

    pub fn on_ball_events(&self) -> &[DodgeRefreshedEvent] {
        &self.on_ball_events
    }

    pub fn confirmed_flip_reset_events(&self) -> &[ConfirmedFlipResetEvent] {
        &self.confirmed_flip_reset_events
    }

    fn player<'a>(players: &'a PlayerFrameState, player_id: &PlayerId) -> Option<&'a PlayerSample> {
        players
            .players
            .iter()
            .find(|player| &player.player_id == player_id)
    }

    fn player_is_grounded(players: &PlayerFrameState, player_id: &PlayerId) -> bool {
        Self::player(players, player_id)
            .and_then(PlayerSample::position)
            .is_some_and(|position| position.z <= FLIP_RESET_GROUNDED_Z)
    }

    fn player_dodge_active(players: &PlayerFrameState, player_id: &PlayerId) -> bool {
        Self::player(players, player_id).is_some_and(|player| player.dodge_active)
    }

    fn on_ball_dodge_reset(
        ball: &BallFrameState,
        players: &PlayerFrameState,
        player_id: &PlayerId,
    ) -> bool {
        const MIN_PLAYER_HEIGHT: f32 = 95.0;
        const MIN_BALL_HEIGHT: f32 = 80.0;
        const MAX_CENTER_DISTANCE: f32 = 180.0;
        const MAX_LOCAL_VERTICAL_OFFSET: f32 = 140.0;

        let Some(ball) = ball.sample() else {
            return false;
        };
        let Some(player) = Self::player(players, player_id) else {
            return false;
        };
        let Some(player_rigid_body) = &player.rigid_body else {
            return false;
        };

        let ball_position = vec_to_glam(&ball.rigid_body.location);
        let player_position = vec_to_glam(&player_rigid_body.location);
        if player_position.z < MIN_PLAYER_HEIGHT || ball_position.z < MIN_BALL_HEIGHT {
            return false;
        }

        let relative_ball_position = ball_position - player_position;
        let center_distance = relative_ball_position.length();
        if !center_distance.is_finite() || center_distance > MAX_CENTER_DISTANCE {
            return false;
        }

        let player_rotation = quat_to_glam(&player_rigid_body.rotation);
        let local_ball_position = player_rotation.inverse() * relative_ball_position;
        local_ball_position.z <= MAX_LOCAL_VERTICAL_OFFSET
    }

    fn prune_pending_resets(&mut self, players: &PlayerFrameState) {
        let grounded_players = self
            .pending_on_ball_resets
            .keys()
            .filter(|player_id| Self::player_is_grounded(players, player_id))
            .cloned()
            .collect::<Vec<_>>();
        for player_id in grounded_players {
            self.pending_on_ball_resets.remove(&player_id);
            self.pending_reset_dodge_started.remove(&player_id);
        }
    }

    fn update_pending_reset_dodges(&mut self, players: &PlayerFrameState) {
        for player in &players.players {
            let was_dodge_active = self
                .previous_dodge_active
                .insert(player.player_id.clone(), player.dodge_active)
                .unwrap_or(false);
            if player.dodge_active
                && !was_dodge_active
                && self.pending_on_ball_resets.contains_key(&player.player_id)
            {
                self.pending_reset_dodge_started
                    .insert(player.player_id.clone());
            }
        }
    }

    fn apply_confirmed_flip_reset_touch(
        &mut self,
        players: &PlayerFrameState,
        touch_event: &TouchEvent,
    ) {
        let Some(player_id) = touch_event.player.as_ref() else {
            return;
        };
        if !self.pending_reset_dodge_started.contains(player_id)
            || !Self::player_dodge_active(players, player_id)
        {
            return;
        }

        let Some(reset_event) = self.pending_on_ball_resets.get(player_id).cloned() else {
            return;
        };
        let time_since_reset = touch_event.time - reset_event.time;
        if !(FLIP_RESET_MIN_DODGE_TOUCH_DELAY_SECONDS..=FLIP_RESET_MAX_DODGE_TOUCH_DELAY_SECONDS)
            .contains(&time_since_reset)
        {
            if time_since_reset > FLIP_RESET_MAX_DODGE_TOUCH_DELAY_SECONDS {
                self.pending_on_ball_resets.remove(player_id);
                self.pending_reset_dodge_started.remove(player_id);
            }
            return;
        }

        self.confirmed_flip_reset_events
            .push(ConfirmedFlipResetEvent {
                time: touch_event.time,
                frame: touch_event.frame,
                reset_time: reset_event.time,
                reset_frame: reset_event.frame,
                player: player_id.clone(),
                is_team_0: touch_event.team_is_team_0,
                counter_value: reset_event.counter_value,
                time_since_reset,
            });
        self.pending_on_ball_resets.remove(player_id);
        self.pending_reset_dodge_started.remove(player_id);
    }

    pub fn update(
        &mut self,
        ball: &BallFrameState,
        players: &PlayerFrameState,
        events: &FrameEventsState,
    ) -> SubtrActorResult<()> {
        self.prune_pending_resets(players);
        for event in &events.dodge_refreshed_events {
            let on_ball = Self::on_ball_dodge_reset(ball, players, &event.player);
            let stats = self.player_stats.entry(event.player.clone()).or_default();
            stats.count += 1;
            if on_ball {
                stats.on_ball_count += 1;
                self.on_ball_events.push(event.clone());
                self.pending_on_ball_resets
                    .insert(event.player.clone(), event.clone());
                self.pending_reset_dodge_started.remove(&event.player);
            }
            self.events.push(DodgeResetEvent {
                time: event.time,
                frame: event.frame,
                player: event.player.clone(),
                is_team_0: event.is_team_0,
                counter_value: event.counter_value,
                on_ball,
            });
        }
        self.update_pending_reset_dodges(players);
        for touch_event in &events.touch_events {
            self.apply_confirmed_flip_reset_touch(players, touch_event);
        }
        Ok(())
    }
}

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