subtr-actor 0.9.1

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(z: f32) -> BallFrameState {
    BallFrameState::Present(BallSample {
        rigid_body: rigid_body(glam::Vec3::new(0.0, 0.0, z)),
    })
}

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

fn player(player_id: PlayerId, is_team_0: bool, goals: i32) -> PlayerSample {
    PlayerSample {
        player_id,
        is_team_0,
        rigid_body: Some(rigid_body(glam::Vec3::new(0.0, 0.0, 17.0))),
        boost_amount: Some(33.0),
        last_boost_amount: None,
        boost_active: false,
        dodge_active: false,
        powerslide_active: false,
        match_goals: Some(goals),
        match_assists: Some(0),
        match_saves: Some(0),
        match_shots: Some(0),
        match_score: Some(0),
    }
}

fn goal_event(time: f32, frame: usize, scorer: PlayerId) -> GoalEvent {
    GoalEvent {
        time,
        frame,
        scoring_team_is_team_0: true,
        player: Some(scorer),
        team_zero_score: Some(1),
        team_one_score: Some(0),
    }
}

fn buildup_sample(time: f32, ball_y: f32) -> GoalBuildupSample {
    GoalBuildupSample {
        time,
        dt: 1.0,
        ball_y,
    }
}

fn shot_pressure(time: f32, is_team_0: bool) -> GoalBuildupPressureEvent {
    GoalBuildupPressureEvent { time, is_team_0 }
}

fn update(
    calculator: &mut MatchStatsCalculator,
    frame: FrameInfo,
    ball: BallFrameState,
    player_goals: i32,
    goal_events: Vec<GoalEvent>,
) {
    let scorer = PlayerId::Steam(1);
    calculator
        .update_parts(
            &frame,
            &GameplayState {
                ball_has_been_hit: Some(true),
                team_zero_score: Some(player_goals),
                team_one_score: Some(0),
                ..GameplayState::default()
            },
            &ball,
            &PlayerFrameState {
                players: vec![player(scorer, true, player_goals)],
            },
            &FrameEventsState {
                goal_events,
                ..FrameEventsState::default()
            },
            &LivePlayState {
                gameplay_phase: GameplayPhase::ActivePlay,
                is_live_play: true,
            },
            &TouchState::default(),
        )
        .unwrap();
}

#[test]
fn records_goal_ball_air_time_from_last_ground_contact() {
    let scorer = PlayerId::Steam(1);
    let mut calculator = MatchStatsCalculator::new();

    update(
        &mut calculator,
        frame(0, 0.0),
        ball(BALL_GROUND_CONTACT_MAX_Z),
        0,
        Vec::new(),
    );
    update(
        &mut calculator,
        frame(1, 1.0),
        ball(BALL_GROUND_CONTACT_MAX_Z + 200.0),
        0,
        Vec::new(),
    );
    update(
        &mut calculator,
        frame(2, 2.5),
        ball(BALL_GROUND_CONTACT_MAX_Z + 300.0),
        1,
        vec![goal_event(2.5, 2, scorer.clone())],
    );

    assert_eq!(
        calculator.goal_context_events()[0].ball_air_time_before_goal,
        Some(2.5)
    );

    let scorer_stats = calculator.player_stats().get(&scorer).unwrap();
    assert_eq!(
        scorer_stats
            .scoring_context
            .goal_ball_air_time
            .goal_ball_air_time_sample_count,
        1
    );
    assert_eq!(scorer_stats.average_goal_ball_air_time(), 2.5);
    assert_eq!(
        calculator.team_zero_stats().average_goal_ball_air_time(),
        2.5
    );
}

#[test]
fn leaves_goal_ball_air_time_empty_without_observed_ground_contact() {
    let scorer = PlayerId::Steam(1);
    let mut calculator = MatchStatsCalculator::new();

    update(
        &mut calculator,
        frame(2, 2.5),
        ball(BALL_GROUND_CONTACT_MAX_Z + 300.0),
        1,
        vec![goal_event(2.5, 2, scorer.clone())],
    );

    assert_eq!(
        calculator.goal_context_events()[0].ball_air_time_before_goal,
        None
    );

    let scorer_stats = calculator.player_stats().get(&scorer).unwrap();
    assert_eq!(
        scorer_stats
            .scoring_context
            .goal_ball_air_time
            .goal_ball_air_time_sample_count,
        0
    );
}

#[test]
fn counter_attack_buildup_accepts_defensive_half_pressure_without_defensive_third_time() {
    let mut calculator = MatchStatsCalculator::new();
    calculator.goal_buildup_samples = vec![
        buildup_sample(2.0, -500.0),
        buildup_sample(3.0, -500.0),
        buildup_sample(4.0, -500.0),
        buildup_sample(5.0, -500.0),
        buildup_sample(6.0, 600.0),
        buildup_sample(7.0, 600.0),
        buildup_sample(8.0, 600.0),
        buildup_sample(9.0, 600.0),
    ];

    assert_eq!(
        calculator.classify_goal_buildup(10.0, true),
        GoalBuildupKind::CounterAttack
    );
}

#[test]
fn counter_attack_buildup_accepts_opponent_shot_as_pressure_signal() {
    let mut calculator = MatchStatsCalculator::new();
    calculator.goal_buildup_samples = vec![
        buildup_sample(6.0, 600.0),
        buildup_sample(7.0, 600.0),
        buildup_sample(8.0, 600.0),
        buildup_sample(9.0, 600.0),
    ];
    calculator.goal_buildup_pressure_events = vec![shot_pressure(7.5, false)];

    assert_eq!(
        calculator.classify_goal_buildup(10.0, true),
        GoalBuildupKind::CounterAttack
    );
}

#[test]
fn counter_attack_buildup_requires_a_defensive_pressure_signal() {
    let mut calculator = MatchStatsCalculator::new();
    calculator.goal_buildup_samples = vec![
        buildup_sample(6.0, 600.0),
        buildup_sample(7.0, 600.0),
        buildup_sample(8.0, 600.0),
        buildup_sample(9.0, 600.0),
    ];

    assert_eq!(
        calculator.classify_goal_buildup(10.0, true),
        GoalBuildupKind::Other
    );
}