use super::*;
pub(crate) const FIFTY_FIFTY_CONTINUATION_TOUCH_WINDOW_SECONDS: f32 = 0.2;
pub(crate) const FIFTY_FIFTY_RESOLUTION_DELAY_SECONDS: f32 = 0.35;
pub(crate) const FIFTY_FIFTY_MAX_DURATION_SECONDS: f32 = 1.25;
pub(crate) const FIFTY_FIFTY_MIN_EXIT_DISTANCE: f32 = 180.0;
pub(crate) const FIFTY_FIFTY_MIN_EXIT_SPEED: f32 = 220.0;
#[derive(Debug, Clone, Default, PartialEq)]
pub struct FiftyFiftyState {
pub active_event: Option<ActiveFiftyFifty>,
pub resolved_events: Vec<FiftyFiftyEvent>,
pub last_resolved_event: Option<FiftyFiftyEvent>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ActiveFiftyFifty {
pub start_time: f32,
pub start_frame: usize,
pub last_touch_time: f32,
pub last_touch_frame: usize,
pub is_kickoff: bool,
pub team_zero_player: Option<PlayerId>,
pub team_one_player: Option<PlayerId>,
pub team_zero_touch_time: Option<f32>,
pub team_zero_touch_frame: Option<usize>,
pub team_zero_dodge_contact: bool,
pub team_one_touch_time: Option<f32>,
pub team_one_touch_frame: Option<usize>,
pub team_one_dodge_contact: bool,
pub team_zero_position: [f32; 3],
pub team_one_position: [f32; 3],
pub midpoint: [f32; 3],
pub plane_normal: [f32; 3],
}
impl ActiveFiftyFifty {
pub fn midpoint_vec(&self) -> glam::Vec3 {
glam::Vec3::from_array(self.midpoint)
}
pub fn plane_normal_vec(&self) -> glam::Vec3 {
glam::Vec3::from_array(self.plane_normal)
}
pub fn contains_team_touch(&self, touch_events: &[TouchEvent]) -> bool {
self.latest_continuation_touch(touch_events).is_some()
}
pub fn latest_continuation_touch<'a>(
&self,
touch_events: &'a [TouchEvent],
) -> Option<&'a TouchEvent> {
touch_events
.iter()
.filter(|touch| {
(touch.team_is_team_0 && self.team_zero_player.is_some())
|| (!touch.team_is_team_0 && self.team_one_player.is_some())
})
.max_by(|left, right| TouchEvent::timestamp_ordering(left, right))
}
}
#[cfg(test)]
#[path = "fifty_fifty_tests.rs"]
mod tests;
#[derive(Debug, Clone, PartialEq, Serialize, ts_rs::TS)]
#[ts(export)]
pub struct FiftyFiftyEvent {
pub start_time: f32,
pub start_frame: usize,
pub resolve_time: f32,
pub resolve_frame: usize,
pub is_kickoff: bool,
#[ts(as = "Option<crate::interop::ts_bindings::RemoteIdTs>")]
pub team_zero_player: Option<PlayerId>,
#[ts(as = "Option<crate::interop::ts_bindings::RemoteIdTs>")]
pub team_one_player: Option<PlayerId>,
pub team_zero_touch_time: Option<f32>,
pub team_zero_touch_frame: Option<usize>,
pub team_zero_dodge_contact: bool,
pub team_one_touch_time: Option<f32>,
pub team_one_touch_frame: Option<usize>,
pub team_one_dodge_contact: bool,
pub team_zero_position: [f32; 3],
pub team_one_position: [f32; 3],
pub midpoint: [f32; 3],
pub plane_normal: [f32; 3],
pub winning_team_is_team_0: Option<bool>,
pub possession_team_is_team_0: Option<bool>,
}
pub(crate) const FIFTY_FIFTY_PHASE_LABELS: [StatLabel; 2] = [
StatLabel::new("phase", "open_play"),
StatLabel::new("phase", "kickoff"),
];
pub(crate) const FIFTY_FIFTY_TEAM_OUTCOME_LABELS: [StatLabel; 3] = [
StatLabel::new("winning_team", "team_zero"),
StatLabel::new("winning_team", "team_one"),
StatLabel::new("winning_team", "neutral"),
];
pub(crate) const FIFTY_FIFTY_POSSESSION_LABELS: [StatLabel; 3] = [
StatLabel::new("possession_after", "team_zero"),
StatLabel::new("possession_after", "team_one"),
StatLabel::new("possession_after", "neutral"),
];
pub(crate) const FIFTY_FIFTY_PLAYER_OUTCOME_LABELS: [StatLabel; 3] = [
StatLabel::new("outcome", "win"),
StatLabel::new("outcome", "loss"),
StatLabel::new("outcome", "neutral"),
];
pub(crate) const FIFTY_FIFTY_PLAYER_POSSESSION_LABELS: [StatLabel; 3] = [
StatLabel::new("possession_after", "self"),
StatLabel::new("possession_after", "opponent"),
StatLabel::new("possession_after", "neutral"),
];
pub(crate) const FIFTY_FIFTY_TOUCH_DODGE_STATE_LABELS: [StatLabel; 2] = [
StatLabel::new("dodge_state", "no_dodge"),
StatLabel::new("dodge_state", "dodge"),
];
pub(crate) const FIFTY_FIFTY_TEAM_ZERO_DODGE_STATE_LABELS: [StatLabel; 2] = [
StatLabel::new("team_zero_dodge_state", "no_dodge"),
StatLabel::new("team_zero_dodge_state", "dodge"),
];
pub(crate) const FIFTY_FIFTY_TEAM_ONE_DODGE_STATE_LABELS: [StatLabel; 2] = [
StatLabel::new("team_one_dodge_state", "no_dodge"),
StatLabel::new("team_one_dodge_state", "dodge"),
];
pub(crate) fn fifty_fifty_phase_label(is_kickoff: bool) -> StatLabel {
if is_kickoff {
StatLabel::new("phase", "kickoff")
} else {
StatLabel::new("phase", "open_play")
}
}
pub(crate) fn fifty_fifty_team_outcome_label(team_is_team_0: Option<bool>) -> StatLabel {
match team_is_team_0 {
Some(true) => StatLabel::new("winning_team", "team_zero"),
Some(false) => StatLabel::new("winning_team", "team_one"),
None => StatLabel::new("winning_team", "neutral"),
}
}
pub(crate) fn fifty_fifty_possession_label(team_is_team_0: Option<bool>) -> StatLabel {
match team_is_team_0 {
Some(true) => StatLabel::new("possession_after", "team_zero"),
Some(false) => StatLabel::new("possession_after", "team_one"),
None => StatLabel::new("possession_after", "neutral"),
}
}
pub(crate) fn fifty_fifty_player_outcome_label(
player_team_is_team_0: bool,
winning_team_is_team_0: Option<bool>,
) -> StatLabel {
match winning_team_is_team_0 {
Some(team_is_team_0) if team_is_team_0 == player_team_is_team_0 => {
StatLabel::new("outcome", "win")
}
Some(_) => StatLabel::new("outcome", "loss"),
None => StatLabel::new("outcome", "neutral"),
}
}
pub(crate) fn fifty_fifty_player_possession_label(
player_team_is_team_0: bool,
possession_team_is_team_0: Option<bool>,
) -> StatLabel {
match possession_team_is_team_0 {
Some(team_is_team_0) if team_is_team_0 == player_team_is_team_0 => {
StatLabel::new("possession_after", "self")
}
Some(_) => StatLabel::new("possession_after", "opponent"),
None => StatLabel::new("possession_after", "neutral"),
}
}
pub(crate) fn fifty_fifty_touch_dodge_state_label(dodge_contact: bool) -> StatLabel {
if dodge_contact {
StatLabel::new("dodge_state", "dodge")
} else {
StatLabel::new("dodge_state", "no_dodge")
}
}
pub(crate) fn fifty_fifty_team_zero_dodge_state_label(dodge_contact: bool) -> StatLabel {
if dodge_contact {
StatLabel::new("team_zero_dodge_state", "dodge")
} else {
StatLabel::new("team_zero_dodge_state", "no_dodge")
}
}
pub(crate) fn fifty_fifty_team_one_dodge_state_label(dodge_contact: bool) -> StatLabel {
if dodge_contact {
StatLabel::new("team_one_dodge_state", "dodge")
} else {
StatLabel::new("team_one_dodge_state", "no_dodge")
}
}
impl FiftyFiftyEvent {
pub(crate) fn labels(&self) -> Vec<StatLabel> {
vec![
fifty_fifty_phase_label(self.is_kickoff),
fifty_fifty_team_outcome_label(self.winning_team_is_team_0),
fifty_fifty_possession_label(self.possession_team_is_team_0),
fifty_fifty_team_zero_dodge_state_label(self.team_zero_dodge_contact),
fifty_fifty_team_one_dodge_state_label(self.team_one_dodge_contact),
]
}
pub(crate) fn player_labels(&self, player_team_is_team_0: bool) -> Vec<StatLabel> {
let dodge_contact = if player_team_is_team_0 {
self.team_zero_dodge_contact
} else {
self.team_one_dodge_contact
};
vec![
fifty_fifty_phase_label(self.is_kickoff),
fifty_fifty_player_outcome_label(player_team_is_team_0, self.winning_team_is_team_0),
fifty_fifty_player_possession_label(
player_team_is_team_0,
self.possession_team_is_team_0,
),
fifty_fifty_touch_dodge_state_label(dodge_contact),
]
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct FiftyFiftyCalculator {
events: EventStream<FiftyFiftyEvent>,
}
impl FiftyFiftyCalculator {
pub fn new() -> Self {
Self::default()
}
pub fn events(&self) -> &[FiftyFiftyEvent] {
self.events.all()
}
pub fn new_events(&self) -> &[FiftyFiftyEvent] {
self.events.new_events()
}
fn apply_event(&mut self, event: &FiftyFiftyEvent) {
self.events.push(event.clone());
}
pub(crate) fn kickoff_phase_active(gameplay: &GameplayState) -> bool {
gameplay.kickoff_phase_active()
}
fn latest_touch_for_team(
touch_events: &[TouchEvent],
team_is_team_0: bool,
) -> Option<&TouchEvent> {
touch_events
.iter()
.filter(|touch| touch.team_is_team_0 == team_is_team_0)
.max_by(|left, right| TouchEvent::timestamp_ordering(left, right))
}
pub(crate) fn contested_touch(
frame: &FrameInfo,
players: &PlayerFrameState,
touch_events: &[TouchEvent],
is_kickoff: bool,
) -> Option<ActiveFiftyFifty> {
let team_zero_touch = Self::latest_touch_for_team(touch_events, true)?;
let team_one_touch = Self::latest_touch_for_team(touch_events, false)?;
let team_zero_position = team_zero_touch.player.as_ref().and_then(|player_id| {
players
.players
.iter()
.find(|player| &player.player_id == player_id)
.and_then(PlayerSample::position)
})?;
let team_one_position = team_one_touch.player.as_ref().and_then(|player_id| {
players
.players
.iter()
.find(|player| &player.player_id == player_id)
.and_then(PlayerSample::position)
})?;
let midpoint = (team_zero_position + team_one_position) * 0.5;
let mut plane_normal = team_one_position - team_zero_position;
plane_normal.z = 0.0;
if plane_normal.length_squared() <= f32::EPSILON {
plane_normal = glam::Vec3::Y;
} else {
plane_normal = plane_normal.normalize();
}
let last_touch = [team_zero_touch, team_one_touch]
.into_iter()
.max_by(|left, right| TouchEvent::timestamp_ordering(left, right))?;
Some(ActiveFiftyFifty {
start_time: frame.time,
start_frame: frame.frame_number,
last_touch_time: last_touch.time,
last_touch_frame: last_touch.frame,
is_kickoff,
team_zero_player: team_zero_touch.player.clone(),
team_one_player: team_one_touch.player.clone(),
team_zero_touch_time: Some(team_zero_touch.time),
team_zero_touch_frame: Some(team_zero_touch.frame),
team_zero_dodge_contact: team_zero_touch.dodge_contact,
team_one_touch_time: Some(team_one_touch.time),
team_one_touch_frame: Some(team_one_touch.frame),
team_one_dodge_contact: team_one_touch.dodge_contact,
team_zero_position: team_zero_position.to_array(),
team_one_position: team_one_position.to_array(),
midpoint: midpoint.to_array(),
plane_normal: plane_normal.to_array(),
})
}
pub(crate) fn winning_team_from_ball(
active: &ActiveFiftyFifty,
ball: &BallFrameState,
) -> Option<bool> {
let ball = ball.sample()?;
let midpoint = active.midpoint_vec();
let plane_normal = active.plane_normal_vec();
let displacement = ball.position() - midpoint;
let signed_distance = displacement.dot(plane_normal);
if signed_distance.abs() >= FIFTY_FIFTY_MIN_EXIT_DISTANCE {
return Some(signed_distance > 0.0);
}
let signed_speed = ball.velocity().dot(plane_normal);
if signed_speed.abs() >= FIFTY_FIFTY_MIN_EXIT_SPEED {
return Some(signed_speed > 0.0);
}
None
}
pub fn update(&mut self, fifty_fifty_state: &FiftyFiftyState) -> SubtrActorResult<()> {
self.events.begin_update();
for event in &fifty_fifty_state.resolved_events {
self.apply_event(event);
}
Ok(())
}
}