1use super::*;
2
3pub(crate) const FIFTY_FIFTY_CONTINUATION_TOUCH_WINDOW_SECONDS: f32 = 0.2;
4pub(crate) const FIFTY_FIFTY_RESOLUTION_DELAY_SECONDS: f32 = 0.35;
5pub(crate) const FIFTY_FIFTY_MAX_DURATION_SECONDS: f32 = 1.25;
6pub(crate) const FIFTY_FIFTY_MIN_EXIT_DISTANCE: f32 = 180.0;
7pub(crate) const FIFTY_FIFTY_MIN_EXIT_SPEED: f32 = 220.0;
8
9#[derive(Debug, Clone, Default, PartialEq)]
10pub struct FiftyFiftyState {
11 pub active_event: Option<ActiveFiftyFifty>,
12 pub resolved_events: Vec<FiftyFiftyEvent>,
13 pub last_resolved_event: Option<FiftyFiftyEvent>,
14}
15
16#[derive(Debug, Clone, PartialEq)]
17pub struct ActiveFiftyFifty {
18 pub start_time: f32,
19 pub start_frame: usize,
20 pub last_touch_time: f32,
21 pub last_touch_frame: usize,
22 pub is_kickoff: bool,
23 pub team_zero_player: Option<PlayerId>,
24 pub team_one_player: Option<PlayerId>,
25 pub team_zero_touch_time: Option<f32>,
26 pub team_zero_touch_frame: Option<usize>,
27 pub team_zero_dodge_contact: bool,
28 pub team_one_touch_time: Option<f32>,
29 pub team_one_touch_frame: Option<usize>,
30 pub team_one_dodge_contact: bool,
31 pub team_zero_position: [f32; 3],
32 pub team_one_position: [f32; 3],
33 pub midpoint: [f32; 3],
34 pub plane_normal: [f32; 3],
35}
36
37impl ActiveFiftyFifty {
38 pub fn midpoint_vec(&self) -> glam::Vec3 {
39 glam::Vec3::from_array(self.midpoint)
40 }
41
42 pub fn plane_normal_vec(&self) -> glam::Vec3 {
43 glam::Vec3::from_array(self.plane_normal)
44 }
45
46 pub fn contains_team_touch(&self, touch_events: &[TouchEvent]) -> bool {
47 touch_events.iter().any(|touch| {
48 (touch.team_is_team_0 && self.team_zero_player.is_some())
49 || (!touch.team_is_team_0 && self.team_one_player.is_some())
50 })
51 }
52}
53
54#[cfg(test)]
55#[path = "fifty_fifty_tests.rs"]
56mod tests;
57
58#[derive(Debug, Clone, PartialEq, Serialize, ts_rs::TS)]
59#[ts(export)]
60pub struct FiftyFiftyEvent {
61 pub start_time: f32,
62 pub start_frame: usize,
63 pub resolve_time: f32,
64 pub resolve_frame: usize,
65 pub is_kickoff: bool,
66 #[ts(as = "Option<crate::ts_bindings::RemoteIdTs>")]
67 pub team_zero_player: Option<PlayerId>,
68 #[ts(as = "Option<crate::ts_bindings::RemoteIdTs>")]
69 pub team_one_player: Option<PlayerId>,
70 pub team_zero_touch_time: Option<f32>,
71 pub team_zero_touch_frame: Option<usize>,
72 pub team_zero_dodge_contact: bool,
73 pub team_one_touch_time: Option<f32>,
74 pub team_one_touch_frame: Option<usize>,
75 pub team_one_dodge_contact: bool,
76 pub team_zero_position: [f32; 3],
77 pub team_one_position: [f32; 3],
78 pub midpoint: [f32; 3],
79 pub plane_normal: [f32; 3],
80 pub winning_team_is_team_0: Option<bool>,
81 pub possession_team_is_team_0: Option<bool>,
82}
83
84const FIFTY_FIFTY_PHASE_LABELS: [StatLabel; 2] = [
85 StatLabel::new("phase", "open_play"),
86 StatLabel::new("phase", "kickoff"),
87];
88const FIFTY_FIFTY_TEAM_OUTCOME_LABELS: [StatLabel; 3] = [
89 StatLabel::new("winning_team", "team_zero"),
90 StatLabel::new("winning_team", "team_one"),
91 StatLabel::new("winning_team", "neutral"),
92];
93const FIFTY_FIFTY_POSSESSION_LABELS: [StatLabel; 3] = [
94 StatLabel::new("possession_after", "team_zero"),
95 StatLabel::new("possession_after", "team_one"),
96 StatLabel::new("possession_after", "neutral"),
97];
98const FIFTY_FIFTY_PLAYER_OUTCOME_LABELS: [StatLabel; 3] = [
99 StatLabel::new("outcome", "win"),
100 StatLabel::new("outcome", "loss"),
101 StatLabel::new("outcome", "neutral"),
102];
103const FIFTY_FIFTY_PLAYER_POSSESSION_LABELS: [StatLabel; 3] = [
104 StatLabel::new("possession_after", "self"),
105 StatLabel::new("possession_after", "opponent"),
106 StatLabel::new("possession_after", "neutral"),
107];
108const FIFTY_FIFTY_TOUCH_DODGE_STATE_LABELS: [StatLabel; 2] = [
109 StatLabel::new("dodge_state", "no_dodge"),
110 StatLabel::new("dodge_state", "dodge"),
111];
112const FIFTY_FIFTY_TEAM_ZERO_DODGE_STATE_LABELS: [StatLabel; 2] = [
113 StatLabel::new("team_zero_dodge_state", "no_dodge"),
114 StatLabel::new("team_zero_dodge_state", "dodge"),
115];
116const FIFTY_FIFTY_TEAM_ONE_DODGE_STATE_LABELS: [StatLabel; 2] = [
117 StatLabel::new("team_one_dodge_state", "no_dodge"),
118 StatLabel::new("team_one_dodge_state", "dodge"),
119];
120
121fn fifty_fifty_phase_label(is_kickoff: bool) -> StatLabel {
122 if is_kickoff {
123 StatLabel::new("phase", "kickoff")
124 } else {
125 StatLabel::new("phase", "open_play")
126 }
127}
128
129fn fifty_fifty_team_outcome_label(team_is_team_0: Option<bool>) -> StatLabel {
130 match team_is_team_0 {
131 Some(true) => StatLabel::new("winning_team", "team_zero"),
132 Some(false) => StatLabel::new("winning_team", "team_one"),
133 None => StatLabel::new("winning_team", "neutral"),
134 }
135}
136
137fn fifty_fifty_possession_label(team_is_team_0: Option<bool>) -> StatLabel {
138 match team_is_team_0 {
139 Some(true) => StatLabel::new("possession_after", "team_zero"),
140 Some(false) => StatLabel::new("possession_after", "team_one"),
141 None => StatLabel::new("possession_after", "neutral"),
142 }
143}
144
145fn fifty_fifty_player_outcome_label(
146 player_team_is_team_0: bool,
147 winning_team_is_team_0: Option<bool>,
148) -> StatLabel {
149 match winning_team_is_team_0 {
150 Some(team_is_team_0) if team_is_team_0 == player_team_is_team_0 => {
151 StatLabel::new("outcome", "win")
152 }
153 Some(_) => StatLabel::new("outcome", "loss"),
154 None => StatLabel::new("outcome", "neutral"),
155 }
156}
157
158fn fifty_fifty_player_possession_label(
159 player_team_is_team_0: bool,
160 possession_team_is_team_0: Option<bool>,
161) -> StatLabel {
162 match possession_team_is_team_0 {
163 Some(team_is_team_0) if team_is_team_0 == player_team_is_team_0 => {
164 StatLabel::new("possession_after", "self")
165 }
166 Some(_) => StatLabel::new("possession_after", "opponent"),
167 None => StatLabel::new("possession_after", "neutral"),
168 }
169}
170
171fn fifty_fifty_touch_dodge_state_label(dodge_contact: bool) -> StatLabel {
172 if dodge_contact {
173 StatLabel::new("dodge_state", "dodge")
174 } else {
175 StatLabel::new("dodge_state", "no_dodge")
176 }
177}
178
179fn fifty_fifty_team_zero_dodge_state_label(dodge_contact: bool) -> StatLabel {
180 if dodge_contact {
181 StatLabel::new("team_zero_dodge_state", "dodge")
182 } else {
183 StatLabel::new("team_zero_dodge_state", "no_dodge")
184 }
185}
186
187fn fifty_fifty_team_one_dodge_state_label(dodge_contact: bool) -> StatLabel {
188 if dodge_contact {
189 StatLabel::new("team_one_dodge_state", "dodge")
190 } else {
191 StatLabel::new("team_one_dodge_state", "no_dodge")
192 }
193}
194
195impl FiftyFiftyEvent {
196 fn labels(&self) -> Vec<StatLabel> {
197 vec![
198 fifty_fifty_phase_label(self.is_kickoff),
199 fifty_fifty_team_outcome_label(self.winning_team_is_team_0),
200 fifty_fifty_possession_label(self.possession_team_is_team_0),
201 fifty_fifty_team_zero_dodge_state_label(self.team_zero_dodge_contact),
202 fifty_fifty_team_one_dodge_state_label(self.team_one_dodge_contact),
203 ]
204 }
205
206 fn player_labels(&self, player_team_is_team_0: bool) -> Vec<StatLabel> {
207 let dodge_contact = if player_team_is_team_0 {
208 self.team_zero_dodge_contact
209 } else {
210 self.team_one_dodge_contact
211 };
212 vec![
213 fifty_fifty_phase_label(self.is_kickoff),
214 fifty_fifty_player_outcome_label(player_team_is_team_0, self.winning_team_is_team_0),
215 fifty_fifty_player_possession_label(
216 player_team_is_team_0,
217 self.possession_team_is_team_0,
218 ),
219 fifty_fifty_touch_dodge_state_label(dodge_contact),
220 ]
221 }
222}
223
224#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, ts_rs::TS)]
225#[ts(export)]
226pub struct FiftyFiftyStats {
227 pub count: u32,
228 pub team_zero_wins: u32,
229 pub team_one_wins: u32,
230 pub neutral_outcomes: u32,
231 pub kickoff_count: u32,
232 pub kickoff_team_zero_wins: u32,
233 pub kickoff_team_one_wins: u32,
234 pub kickoff_neutral_outcomes: u32,
235 pub team_zero_possession_after_count: u32,
236 pub team_one_possession_after_count: u32,
237 pub neutral_possession_after_count: u32,
238 pub kickoff_team_zero_possession_after_count: u32,
239 pub kickoff_team_one_possession_after_count: u32,
240 pub kickoff_neutral_possession_after_count: u32,
241 #[serde(default, skip_serializing_if = "LabeledCounts::is_empty")]
242 pub labeled_event_counts: LabeledCounts,
243}
244
245#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, ts_rs::TS)]
246#[ts(export)]
247pub struct FiftyFiftyPlayerStats {
248 pub count: u32,
249 pub wins: u32,
250 pub losses: u32,
251 pub neutral_outcomes: u32,
252 pub kickoff_count: u32,
253 pub kickoff_wins: u32,
254 pub kickoff_losses: u32,
255 pub kickoff_neutral_outcomes: u32,
256 pub possession_after_count: u32,
257 pub kickoff_possession_after_count: u32,
258 #[serde(default, skip_serializing_if = "LabeledCounts::is_empty")]
259 pub labeled_event_counts: LabeledCounts,
260}
261
262impl FiftyFiftyStats {
263 fn record_event(&mut self, event: &FiftyFiftyEvent) {
264 self.labeled_event_counts.increment(event.labels());
265 self.sync_legacy_counts();
266 }
267
268 pub fn event_count_with_labels(&self, labels: &[StatLabel]) -> u32 {
269 self.labeled_event_counts.count_matching(labels)
270 }
271
272 pub fn complete_labeled_event_counts(&self) -> LabeledCounts {
273 LabeledCounts::complete_from_label_sets(
274 &[
275 &FIFTY_FIFTY_PHASE_LABELS,
276 &FIFTY_FIFTY_TEAM_OUTCOME_LABELS,
277 &FIFTY_FIFTY_POSSESSION_LABELS,
278 &FIFTY_FIFTY_TEAM_ZERO_DODGE_STATE_LABELS,
279 &FIFTY_FIFTY_TEAM_ONE_DODGE_STATE_LABELS,
280 ],
281 &self.labeled_event_counts,
282 )
283 }
284
285 fn sync_legacy_counts(&mut self) {
286 self.count = self.labeled_event_counts.total();
287 self.team_zero_wins =
288 self.event_count_with_labels(&[fifty_fifty_team_outcome_label(Some(true))]);
289 self.team_one_wins =
290 self.event_count_with_labels(&[fifty_fifty_team_outcome_label(Some(false))]);
291 self.neutral_outcomes =
292 self.event_count_with_labels(&[fifty_fifty_team_outcome_label(None)]);
293 self.kickoff_count = self.event_count_with_labels(&[fifty_fifty_phase_label(true)]);
294 self.kickoff_team_zero_wins = self.event_count_with_labels(&[
295 fifty_fifty_phase_label(true),
296 fifty_fifty_team_outcome_label(Some(true)),
297 ]);
298 self.kickoff_team_one_wins = self.event_count_with_labels(&[
299 fifty_fifty_phase_label(true),
300 fifty_fifty_team_outcome_label(Some(false)),
301 ]);
302 self.kickoff_neutral_outcomes = self.event_count_with_labels(&[
303 fifty_fifty_phase_label(true),
304 fifty_fifty_team_outcome_label(None),
305 ]);
306 self.team_zero_possession_after_count =
307 self.event_count_with_labels(&[fifty_fifty_possession_label(Some(true))]);
308 self.team_one_possession_after_count =
309 self.event_count_with_labels(&[fifty_fifty_possession_label(Some(false))]);
310 self.neutral_possession_after_count =
311 self.event_count_with_labels(&[fifty_fifty_possession_label(None)]);
312 self.kickoff_team_zero_possession_after_count = self.event_count_with_labels(&[
313 fifty_fifty_phase_label(true),
314 fifty_fifty_possession_label(Some(true)),
315 ]);
316 self.kickoff_team_one_possession_after_count = self.event_count_with_labels(&[
317 fifty_fifty_phase_label(true),
318 fifty_fifty_possession_label(Some(false)),
319 ]);
320 self.kickoff_neutral_possession_after_count = self.event_count_with_labels(&[
321 fifty_fifty_phase_label(true),
322 fifty_fifty_possession_label(None),
323 ]);
324 }
325
326 pub fn team_zero_win_pct(&self) -> f32 {
327 if self.count == 0 {
328 0.0
329 } else {
330 self.team_zero_wins as f32 * 100.0 / self.count as f32
331 }
332 }
333
334 pub fn team_one_win_pct(&self) -> f32 {
335 if self.count == 0 {
336 0.0
337 } else {
338 self.team_one_wins as f32 * 100.0 / self.count as f32
339 }
340 }
341
342 pub fn kickoff_team_zero_win_pct(&self) -> f32 {
343 if self.kickoff_count == 0 {
344 0.0
345 } else {
346 self.kickoff_team_zero_wins as f32 * 100.0 / self.kickoff_count as f32
347 }
348 }
349
350 pub fn kickoff_team_one_win_pct(&self) -> f32 {
351 if self.kickoff_count == 0 {
352 0.0
353 } else {
354 self.kickoff_team_one_wins as f32 * 100.0 / self.kickoff_count as f32
355 }
356 }
357}
358
359impl FiftyFiftyPlayerStats {
360 fn record_event(&mut self, player_team_is_team_0: bool, event: &FiftyFiftyEvent) {
361 self.labeled_event_counts
362 .increment(event.player_labels(player_team_is_team_0));
363 self.sync_legacy_counts();
364 }
365
366 pub fn event_count_with_labels(&self, labels: &[StatLabel]) -> u32 {
367 self.labeled_event_counts.count_matching(labels)
368 }
369
370 pub fn complete_labeled_event_counts(&self) -> LabeledCounts {
371 LabeledCounts::complete_from_label_sets(
372 &[
373 &FIFTY_FIFTY_PHASE_LABELS,
374 &FIFTY_FIFTY_PLAYER_OUTCOME_LABELS,
375 &FIFTY_FIFTY_PLAYER_POSSESSION_LABELS,
376 &FIFTY_FIFTY_TOUCH_DODGE_STATE_LABELS,
377 ],
378 &self.labeled_event_counts,
379 )
380 }
381
382 fn sync_legacy_counts(&mut self) {
383 self.count = self.labeled_event_counts.total();
384 self.wins = self.event_count_with_labels(&[StatLabel::new("outcome", "win")]);
385 self.losses = self.event_count_with_labels(&[StatLabel::new("outcome", "loss")]);
386 self.neutral_outcomes =
387 self.event_count_with_labels(&[StatLabel::new("outcome", "neutral")]);
388 self.kickoff_count = self.event_count_with_labels(&[fifty_fifty_phase_label(true)]);
389 self.kickoff_wins = self.event_count_with_labels(&[
390 fifty_fifty_phase_label(true),
391 StatLabel::new("outcome", "win"),
392 ]);
393 self.kickoff_losses = self.event_count_with_labels(&[
394 fifty_fifty_phase_label(true),
395 StatLabel::new("outcome", "loss"),
396 ]);
397 self.kickoff_neutral_outcomes = self.event_count_with_labels(&[
398 fifty_fifty_phase_label(true),
399 StatLabel::new("outcome", "neutral"),
400 ]);
401 self.possession_after_count =
402 self.event_count_with_labels(&[StatLabel::new("possession_after", "self")]);
403 self.kickoff_possession_after_count = self.event_count_with_labels(&[
404 fifty_fifty_phase_label(true),
405 StatLabel::new("possession_after", "self"),
406 ]);
407 }
408
409 pub fn win_pct(&self) -> f32 {
410 if self.count == 0 {
411 0.0
412 } else {
413 self.wins as f32 * 100.0 / self.count as f32
414 }
415 }
416
417 pub fn kickoff_win_pct(&self) -> f32 {
418 if self.kickoff_count == 0 {
419 0.0
420 } else {
421 self.kickoff_wins as f32 * 100.0 / self.kickoff_count as f32
422 }
423 }
424}
425
426#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, ts_rs::TS)]
427#[ts(export)]
428pub struct FiftyFiftyTeamStats {
429 pub count: u32,
430 pub wins: u32,
431 pub losses: u32,
432 pub neutral_outcomes: u32,
433 pub kickoff_count: u32,
434 pub kickoff_wins: u32,
435 pub kickoff_losses: u32,
436 pub kickoff_neutral_outcomes: u32,
437 pub possession_after_count: u32,
438 pub opponent_possession_after_count: u32,
439 pub neutral_possession_after_count: u32,
440 pub kickoff_possession_after_count: u32,
441 pub kickoff_opponent_possession_after_count: u32,
442 pub kickoff_neutral_possession_after_count: u32,
443}
444
445impl FiftyFiftyStats {
446 pub fn for_team(&self, is_team_zero: bool) -> FiftyFiftyTeamStats {
447 let (
448 wins,
449 losses,
450 kickoff_wins,
451 kickoff_losses,
452 possession_after_count,
453 opponent_possession_after_count,
454 kickoff_possession_after_count,
455 kickoff_opponent_possession_after_count,
456 ) = if is_team_zero {
457 (
458 self.team_zero_wins,
459 self.team_one_wins,
460 self.kickoff_team_zero_wins,
461 self.kickoff_team_one_wins,
462 self.team_zero_possession_after_count,
463 self.team_one_possession_after_count,
464 self.kickoff_team_zero_possession_after_count,
465 self.kickoff_team_one_possession_after_count,
466 )
467 } else {
468 (
469 self.team_one_wins,
470 self.team_zero_wins,
471 self.kickoff_team_one_wins,
472 self.kickoff_team_zero_wins,
473 self.team_one_possession_after_count,
474 self.team_zero_possession_after_count,
475 self.kickoff_team_one_possession_after_count,
476 self.kickoff_team_zero_possession_after_count,
477 )
478 };
479
480 FiftyFiftyTeamStats {
481 count: self.count,
482 wins,
483 losses,
484 neutral_outcomes: self.neutral_outcomes,
485 kickoff_count: self.kickoff_count,
486 kickoff_wins,
487 kickoff_losses,
488 kickoff_neutral_outcomes: self.kickoff_neutral_outcomes,
489 possession_after_count,
490 opponent_possession_after_count,
491 neutral_possession_after_count: self.neutral_possession_after_count,
492 kickoff_possession_after_count,
493 kickoff_opponent_possession_after_count,
494 kickoff_neutral_possession_after_count: self.kickoff_neutral_possession_after_count,
495 }
496 }
497}
498
499#[derive(Debug, Clone, Default, PartialEq)]
500pub struct FiftyFiftyCalculator {
501 stats: FiftyFiftyStats,
502 player_stats: HashMap<PlayerId, FiftyFiftyPlayerStats>,
503 events: Vec<FiftyFiftyEvent>,
504}
505
506impl FiftyFiftyCalculator {
507 pub fn new() -> Self {
508 Self::default()
509 }
510
511 pub fn stats(&self) -> &FiftyFiftyStats {
512 &self.stats
513 }
514
515 pub fn player_stats(&self) -> &HashMap<PlayerId, FiftyFiftyPlayerStats> {
516 &self.player_stats
517 }
518
519 pub fn events(&self) -> &[FiftyFiftyEvent] {
520 &self.events
521 }
522
523 fn apply_event(&mut self, event: &FiftyFiftyEvent) {
524 self.stats.record_event(event);
525
526 if let Some(player_id) = event.team_zero_player.as_ref() {
527 let stats = self.player_stats.entry(player_id.clone()).or_default();
528 stats.record_event(true, event);
529 }
530 if let Some(player_id) = event.team_one_player.as_ref() {
531 let stats = self.player_stats.entry(player_id.clone()).or_default();
532 stats.record_event(false, event);
533 }
534
535 self.events.push(event.clone());
536 }
537
538 pub(crate) fn kickoff_phase_active(gameplay: &GameplayState) -> bool {
539 gameplay.game_state == Some(GAME_STATE_KICKOFF_COUNTDOWN)
540 || gameplay.kickoff_countdown_time.is_some_and(|time| time > 0)
541 || gameplay.ball_has_been_hit == Some(false)
542 }
543
544 pub(crate) fn contested_touch(
545 frame: &FrameInfo,
546 players: &PlayerFrameState,
547 touch_events: &[TouchEvent],
548 is_kickoff: bool,
549 ) -> Option<ActiveFiftyFifty> {
550 let team_zero_touch = touch_events.iter().find(|touch| touch.team_is_team_0)?;
551 let team_one_touch = touch_events.iter().find(|touch| !touch.team_is_team_0)?;
552 let team_zero_position = team_zero_touch.player.as_ref().and_then(|player_id| {
553 players
554 .players
555 .iter()
556 .find(|player| &player.player_id == player_id)
557 .and_then(PlayerSample::position)
558 })?;
559 let team_one_position = team_one_touch.player.as_ref().and_then(|player_id| {
560 players
561 .players
562 .iter()
563 .find(|player| &player.player_id == player_id)
564 .and_then(PlayerSample::position)
565 })?;
566 let midpoint = (team_zero_position + team_one_position) * 0.5;
567 let mut plane_normal = team_one_position - team_zero_position;
568 plane_normal.z = 0.0;
569 if plane_normal.length_squared() <= f32::EPSILON {
570 plane_normal = glam::Vec3::Y;
571 } else {
572 plane_normal = plane_normal.normalize();
573 }
574
575 Some(ActiveFiftyFifty {
576 start_time: frame.time,
577 start_frame: frame.frame_number,
578 last_touch_time: frame.time,
579 last_touch_frame: frame.frame_number,
580 is_kickoff,
581 team_zero_player: team_zero_touch.player.clone(),
582 team_one_player: team_one_touch.player.clone(),
583 team_zero_touch_time: Some(team_zero_touch.time),
584 team_zero_touch_frame: Some(team_zero_touch.frame),
585 team_zero_dodge_contact: team_zero_touch.dodge_contact,
586 team_one_touch_time: Some(team_one_touch.time),
587 team_one_touch_frame: Some(team_one_touch.frame),
588 team_one_dodge_contact: team_one_touch.dodge_contact,
589 team_zero_position: team_zero_position.to_array(),
590 team_one_position: team_one_position.to_array(),
591 midpoint: midpoint.to_array(),
592 plane_normal: plane_normal.to_array(),
593 })
594 }
595
596 pub(crate) fn winning_team_from_ball(
597 active: &ActiveFiftyFifty,
598 ball: &BallFrameState,
599 ) -> Option<bool> {
600 let ball = ball.sample()?;
601 let midpoint = active.midpoint_vec();
602 let plane_normal = active.plane_normal_vec();
603 let displacement = ball.position() - midpoint;
604 let signed_distance = displacement.dot(plane_normal);
605 if signed_distance.abs() >= FIFTY_FIFTY_MIN_EXIT_DISTANCE {
606 return Some(signed_distance > 0.0);
607 }
608
609 let signed_speed = ball.velocity().dot(plane_normal);
610 if signed_speed.abs() >= FIFTY_FIFTY_MIN_EXIT_SPEED {
611 return Some(signed_speed > 0.0);
612 }
613
614 None
615 }
616
617 pub fn update(&mut self, fifty_fifty_state: &FiftyFiftyState) -> SubtrActorResult<()> {
618 for event in &fifty_fifty_state.resolved_events {
619 self.apply_event(event);
620 }
621 Ok(())
622 }
623}