subtr-actor 1.1.0

Rocket League replay transformer
Documentation
use super::*;

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

fn touch(frame: usize, time: f32, player: PlayerId, team_is_team_0: bool) -> TouchEvent {
    TouchEvent {
        touch_id: None,
        time,
        frame,
        team_is_team_0,
        player: Some(player),
        player_position: None,
        closest_approach_distance: Some(0.0),
        contact_local_ball_position: None,
        contact_local_hitbox_point: None,
        contact_world_hitbox_point: None,
        dodge_contact: false,
    }
}

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 player(player_id: PlayerId, is_team_0: bool, position: glam::Vec3) -> PlayerSample {
    PlayerSample {
        player_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 two_player_state(team_zero_player: &PlayerId, team_one_player: &PlayerId) -> PlayerFrameState {
    PlayerFrameState {
        players: vec![
            player(
                team_zero_player.clone(),
                true,
                glam::Vec3::new(0.0, -100.0, 0.0),
            ),
            player(
                team_one_player.clone(),
                false,
                glam::Vec3::new(0.0, 100.0, 0.0),
            ),
        ],
    }
}

fn active_event(team_zero_player: PlayerId) -> ActiveFiftyFifty {
    ActiveFiftyFifty {
        start_time: 1.0,
        start_frame: 100,
        last_touch_time: 1.0,
        last_touch_frame: 100,
        is_kickoff: false,
        team_zero_player: Some(team_zero_player),
        team_one_player: None,
        team_zero_touch_time: Some(1.0),
        team_zero_touch_frame: Some(100),
        team_zero_dodge_contact: false,
        team_one_touch_time: None,
        team_one_touch_frame: None,
        team_one_dodge_contact: false,
        team_zero_position: [0.0, 0.0, 0.0],
        team_one_position: [100.0, 0.0, 0.0],
        midpoint: [50.0, 0.0, 0.0],
        plane_normal: [1.0, 0.0, 0.0],
    }
}

#[test]
fn sequential_opposing_touches_start_fifty_fifty_within_short_window() {
    let team_zero_player = PlayerId::Steam(1);
    let team_one_player = PlayerId::Steam(2);
    let players = two_player_state(&team_zero_player, &team_one_player);
    let mut calculator = FiftyFiftyStateCalculator::new();

    let first_state = calculator.update(
        &frame(100, 1.0),
        &GameplayState::default(),
        &BallFrameState::default(),
        &players,
        &TouchState {
            touch_events: vec![touch(100, 1.0, team_zero_player.clone(), true)],
            ..TouchState::default()
        },
        &PossessionState::default(),
        &LivePlayState::active_play(),
    );
    assert!(first_state.active_event.is_none());

    let second_state = calculator.update(
        &frame(103, 1.08),
        &GameplayState::default(),
        &BallFrameState::default(),
        &players,
        &TouchState {
            touch_events: vec![touch(103, 1.08, team_one_player.clone(), false)],
            ..TouchState::default()
        },
        &PossessionState::default(),
        &LivePlayState::active_play(),
    );

    let active = second_state
        .active_event
        .expect("expected sequential opposing touches to start a 50/50");
    assert_eq!(active.start_frame, 100);
    assert_eq!(active.start_time, 1.0);
    assert_eq!(active.team_zero_player, Some(team_zero_player));
    assert_eq!(active.team_one_player, Some(team_one_player));
    assert_eq!(active.team_zero_touch_frame, Some(100));
    assert_eq!(active.team_one_touch_frame, Some(103));
}

#[test]
fn sequential_kickoff_touches_keep_kickoff_phase_from_first_touch() {
    let team_zero_player = PlayerId::Steam(1);
    let team_one_player = PlayerId::Steam(2);
    let players = two_player_state(&team_zero_player, &team_one_player);
    let mut calculator = FiftyFiftyStateCalculator::new();
    let kickoff_gameplay = GameplayState {
        ball_has_been_hit: Some(false),
        ..GameplayState::default()
    };

    calculator.update(
        &frame(100, 1.0),
        &kickoff_gameplay,
        &BallFrameState::default(),
        &players,
        &TouchState {
            touch_events: vec![touch(100, 1.0, team_zero_player, true)],
            ..TouchState::default()
        },
        &PossessionState::default(),
        &LivePlayState {
            gameplay_phase: GameplayPhase::KickoffWaitingForTouch,
            is_live_play: false,
        },
    );
    let state = calculator.update(
        &frame(103, 1.08),
        &GameplayState::default(),
        &BallFrameState::default(),
        &players,
        &TouchState {
            touch_events: vec![touch(103, 1.08, team_one_player, false)],
            ..TouchState::default()
        },
        &PossessionState::default(),
        &LivePlayState::active_play(),
    );

    assert!(
        state
            .active_event
            .expect("expected delayed kickoff contact to start a 50/50")
            .is_kickoff
    );
}

#[test]
fn continuation_touch_updates_last_touch_from_latest_touch_event_not_sample_frame() {
    let player = PlayerId::Steam(1);
    let mut calculator = FiftyFiftyStateCalculator {
        active_event: Some(active_event(player.clone())),
        last_resolved_event: None,
        pending_initial_touch: None,
        kickoff_touch_window_open: false,
    };

    let state = calculator.update(
        &frame(110, 1.1),
        &GameplayState::default(),
        &BallFrameState::default(),
        &PlayerFrameState::default(),
        &TouchState {
            touch_events: vec![
                touch(105, 1.05, player.clone(), true),
                touch(102, 1.02, player, true),
            ],
            ..TouchState::default()
        },
        &PossessionState::default(),
        &LivePlayState::active_play(),
    );

    let active = state
        .active_event
        .expect("expected the fifty-fifty to remain active");
    assert_eq!(active.last_touch_time, 1.05);
    assert_eq!(active.last_touch_frame, 105);
}