subtr-actor 1.0.0

Rocket League replay transformer
Documentation
use super::*;

fn rigid_body(position: glam::Vec3) -> boxcars::RigidBody {
    boxcars::RigidBody {
        sleeping: false,
        location: glam_to_vec(&position),
        rotation: boxcars::Quaternion {
            x: 0.0,
            y: 0.0,
            z: 0.0,
            w: 1.0,
        },
        linear_velocity: Some(glam_to_vec(&glam::Vec3::ZERO)),
        angular_velocity: Some(glam_to_vec(&glam::Vec3::ZERO)),
    }
}

fn ball(position: glam::Vec3) -> BallFrameState {
    BallFrameState::Present(BallSample {
        rigid_body: rigid_body(position),
    })
}

fn player(id: u64, is_team_0: bool, position: glam::Vec3) -> PlayerSample {
    PlayerSample {
        player_id: boxcars::RemoteId::Steam(id),
        is_team_0,
        hitbox: default_car_hitbox(),
        rigid_body: Some(rigid_body(position)),
        boost_amount: None,
        last_boost_amount: None,
        boost_active: false,
        dodge_active: false,
        powerslide_active: false,
        match_goals: None,
        match_assists: None,
        match_saves: None,
        match_shots: None,
        match_score: None,
    }
}

fn frame(frame_number: usize, time: f32) -> FrameInfo {
    FrameInfo {
        frame_number,
        time,
        dt: 0.1,
        seconds_remaining: None,
    }
}

fn active_gameplay() -> GameplayState {
    GameplayState {
        ball_has_been_hit: Some(true),
        current_in_game_team_player_counts: [2, 0],
        ..Default::default()
    }
}

fn ball_depth_segments(margin: f32, start: f32, end: f32) -> Vec<(BallDepthState, f32)> {
    scalar_state_segments(
        start,
        end,
        &[-margin, margin],
        &[
            BallDepthState::BehindBall,
            BallDepthState::LevelWithBall,
            BallDepthState::AheadOfBall,
        ],
    )
}

#[test]
fn ball_depth_segments_treat_near_ball_band_as_level() {
    let segments = ball_depth_segments(150.0, -100.0, 100.0);
    assert_eq!(segments, vec![(BallDepthState::LevelWithBall, 1.0)]);
}

#[test]
fn ball_depth_segments_split_crossing_time_across_all_three_buckets() {
    let segments = ball_depth_segments(150.0, -300.0, 300.0);
    assert_eq!(segments.len(), 3);
    assert_eq!(segments[0].0, BallDepthState::BehindBall);
    assert!((segments[0].1 - 0.25).abs() < 1e-6);
    assert_eq!(segments[1].0, BallDepthState::LevelWithBall);
    assert!((segments[1].1 - 0.5).abs() < 1e-6);
    assert_eq!(segments[2].0, BallDepthState::AheadOfBall);
    assert!((segments[2].1 - 0.25).abs() < 1e-6);
}

#[test]
fn ball_depth_segments_count_boundary_point_as_in_front_not_level() {
    let segments = ball_depth_segments(150.0, 150.0, 150.0);
    assert_eq!(segments, vec![(BallDepthState::AheadOfBall, 1.0)]);
}

#[test]
fn positioning_facets_emit_coalesced_spans() {
    let mut calculator = PositioningCalculator::new();
    let gameplay = active_gameplay();
    let ball = ball(glam::Vec3::ZERO);
    let players = PlayerFrameState {
        players: vec![
            player(1, true, glam::vec3(0.0, -1000.0, 0.0)),
            player(2, true, glam::vec3(0.0, 1000.0, 0.0)),
        ],
    };

    for frame_number in 0..3 {
        calculator
            .update(
                &frame(frame_number, frame_number as f32 * 0.1),
                &gameplay,
                &ball,
                &players,
                &FrameEventsState::default(),
                &LivePlayState::active_play(),
                None,
            )
            .expect("positioning update should succeed");
    }
    calculator.flush_pending_events();

    let activity_events = calculator.activity_events();
    assert_eq!(activity_events.len(), 2);
    for event in &activity_events {
        assert_eq!(event.state, ActivityState::Tracked);
        assert_eq!(event.frame, 0);
        assert_eq!(event.end_frame, 2);
        assert!((event.duration - 0.3).abs() < 1e-6);
    }

    let depth_role_events = calculator.depth_role_events();
    let back_event = depth_role_events
        .iter()
        .find(|event| event.player == boxcars::RemoteId::Steam(1))
        .expect("back player depth role span should be emitted");
    assert_eq!(back_event.state, DepthRoleState::MostBack);
    let forward_event = depth_role_events
        .iter()
        .find(|event| event.player == boxcars::RemoteId::Steam(2))
        .expect("forward player depth role span should be emitted");
    assert_eq!(forward_event.state, DepthRoleState::MostForward);
}

#[test]
fn closest_to_ball_requires_stable_challenger_before_switching() {
    let mut calculator = PositioningCalculator::with_config(PositioningCalculatorConfig {
        closest_to_ball_switch_margin: 0.0,
        closest_to_ball_switch_min_seconds: 0.3,
        ..PositioningCalculatorConfig::default()
    });
    let gameplay = active_gameplay();
    let ball = ball(glam::Vec3::ZERO);

    let closest_player_by_frame = [
        (glam::vec3(100.0, 0.0, 0.0), glam::vec3(120.0, 0.0, 0.0), 1),
        (glam::vec3(100.0, 0.0, 0.0), glam::vec3(50.0, 0.0, 0.0), 1),
        (glam::vec3(100.0, 0.0, 0.0), glam::vec3(50.0, 0.0, 0.0), 1),
        (glam::vec3(100.0, 0.0, 0.0), glam::vec3(50.0, 0.0, 0.0), 2),
    ];

    for (frame_number, (player_one_position, player_two_position, expected_closest)) in
        closest_player_by_frame.iter().enumerate()
    {
        let players = PlayerFrameState {
            players: vec![
                player(1, true, *player_one_position),
                player(2, true, *player_two_position),
            ],
        };
        calculator
            .update(
                &frame(frame_number, frame_number as f32 * 0.1),
                &gameplay,
                &ball,
                &players,
                &FrameEventsState::default(),
                &LivePlayState::active_play(),
                None,
            )
            .expect("positioning update should succeed");

        let events = calculator.ball_proximity_events();
        let closest = events
            .iter()
            .find(|event| event.end_frame == frame_number && event.state.closest_to_ball_team)
            .expect("team closest span should cover the current frame");
        assert_eq!(closest.player, boxcars::RemoteId::Steam(*expected_closest));
        let absolute_closest = events
            .iter()
            .find(|event| event.end_frame == frame_number && event.state.closest_to_ball_absolute)
            .expect("absolute closest span should cover the current frame");
        assert_eq!(
            absolute_closest.player,
            boxcars::RemoteId::Steam(*expected_closest)
        );
    }
}