1use crate::*;
2use serde::Serialize;
3use std::collections::HashMap;
4
5#[derive(Debug, Clone, PartialEq, Serialize, ts_rs::TS)]
6#[ts(export)]
7pub struct FlipResetEvent {
8 pub time: f32,
9 pub frame: usize,
10 #[ts(as = "crate::ts_bindings::RemoteIdTs")]
11 pub player: PlayerId,
12 pub is_team_0: bool,
13 pub confidence: f32,
15 #[ts(as = "crate::ts_bindings::Vector3fTs")]
17 pub local_ball_position: boxcars::Vector3f,
18 pub closest_approach_distance: f32,
20}
21
22#[derive(Debug, Clone, PartialEq, Serialize, ts_rs::TS)]
23#[ts(export)]
24pub struct DodgeRefreshedEvent {
25 pub time: f32,
26 pub frame: usize,
27 #[ts(as = "crate::ts_bindings::RemoteIdTs")]
28 pub player: PlayerId,
29 pub is_team_0: bool,
30 pub counter_value: i32,
31}
32
33#[derive(Debug, Clone, PartialEq, Serialize, ts_rs::TS)]
34#[ts(export)]
35pub struct PostWallDodgeEvent {
36 pub time: f32,
37 pub frame: usize,
38 #[ts(as = "crate::ts_bindings::RemoteIdTs")]
39 pub player: PlayerId,
40 pub is_team_0: bool,
41 pub wall_contact_time: f32,
42 pub time_since_wall_contact: f32,
43}
44
45#[derive(Debug, Clone, PartialEq, Serialize, ts_rs::TS)]
46#[ts(export)]
47pub struct FlipResetFollowupDodgeEvent {
48 pub time: f32,
49 pub frame: usize,
50 #[ts(as = "crate::ts_bindings::RemoteIdTs")]
51 pub player: PlayerId,
52 pub is_team_0: bool,
53 pub candidate_touch_time: f32,
54 pub time_since_candidate_touch: f32,
55 pub candidate_touch_confidence: f32,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq)]
59pub(crate) struct FlipResetHeuristic {
60 pub confidence: f32,
61 pub local_ball_position: glam::Vec3,
62}
63
64#[derive(Debug, Clone, Copy, PartialEq)]
65struct FlipResetTouchFeatures {
66 player_position: glam::Vec3,
67 ball_position: glam::Vec3,
68 center_distance: f32,
69 local_ball_position: glam::Vec3,
70 scaled_touch_distance: f32,
71 underside_alignment: f32,
72}
73
74fn scale_factor_for_positions(ball_position: glam::Vec3, player_position: glam::Vec3) -> f32 {
75 if ball_position
76 .truncate()
77 .abs()
78 .max(player_position.truncate().abs())
79 .max_element()
80 < 200.0
81 {
82 100.0
83 } else {
84 1.0
85 }
86}
87
88fn build_touch_features(
89 ball_body: &boxcars::RigidBody,
90 player_body: &boxcars::RigidBody,
91 closest_approach_distance: f32,
92) -> Option<FlipResetTouchFeatures> {
93 let raw_ball_position = vec_to_glam(&ball_body.location);
94 let raw_player_position = vec_to_glam(&player_body.location);
95 let scale_factor = scale_factor_for_positions(raw_ball_position, raw_player_position);
96
97 let ball_position = raw_ball_position * scale_factor;
98 let player_position = raw_player_position * scale_factor;
99 let relative_ball_position = ball_position - player_position;
100 let center_distance = relative_ball_position.length();
101 if !center_distance.is_finite() || center_distance <= 30.0 || center_distance >= 550.0 {
102 return None;
103 }
104
105 let player_rotation = quat_to_glam(&player_body.rotation);
106 let local_ball_position = player_rotation.inverse() * relative_ball_position;
107 let car_up = (player_rotation * glam::Vec3::Z).normalize_or_zero();
108 let underside_alignment = (-car_up).dot(relative_ball_position.normalize_or_zero());
109 let scaled_touch_distance = closest_approach_distance * scale_factor;
110
111 Some(FlipResetTouchFeatures {
112 player_position,
113 ball_position,
114 center_distance,
115 local_ball_position,
116 scaled_touch_distance,
117 underside_alignment,
118 })
119}
120
121fn flip_reset_confidence(features: &FlipResetTouchFeatures) -> f32 {
122 let below_car_score = (-features.local_ball_position.z / 180.0).clamp(0.0, 1.0);
123 let alignment_score = ((features.underside_alignment - 0.45) / 0.50).clamp(0.0, 1.0);
124 let touch_score = (1.0 - ((features.scaled_touch_distance - 20.0) / 220.0)).clamp(0.0, 1.0);
125 let height_score = ((features.player_position.z - 70.0) / 500.0).clamp(0.0, 1.0);
126 let footprint_score = (1.0
127 - (features.local_ball_position.x.abs() / 260.0).clamp(0.0, 1.0) * 0.5
128 - (features.local_ball_position.y.abs() / 260.0).clamp(0.0, 1.0) * 0.5)
129 .clamp(0.0, 1.0);
130 0.28 * below_car_score
131 + 0.26 * alignment_score
132 + 0.20 * touch_score
133 + 0.14 * height_score
134 + 0.12 * footprint_score
135}
136
137pub(crate) fn flip_reset_candidate(
140 ball_body: &boxcars::RigidBody,
141 player_body: &boxcars::RigidBody,
142 closest_approach_distance: f32,
143) -> Option<FlipResetHeuristic> {
144 const MIN_PLAYER_HEIGHT: f32 = 95.0;
145 const MIN_BALL_HEIGHT: f32 = 80.0;
146 const MAX_TOUCH_DISTANCE: f32 = 220.0;
147 const MIN_UNDERSIDE_ALIGNMENT: f32 = 0.60;
148 const MAX_LOCAL_FORWARD_OFFSET: f32 = 240.0;
149 const MAX_LOCAL_LATERAL_OFFSET: f32 = 240.0;
150 const MIN_CONFIDENCE: f32 = 0.55;
151
152 let features = build_touch_features(ball_body, player_body, closest_approach_distance)?;
153 if features.player_position.z < MIN_PLAYER_HEIGHT || features.ball_position.z < MIN_BALL_HEIGHT
154 {
155 return None;
156 }
157 if features.scaled_touch_distance > MAX_TOUCH_DISTANCE
158 || features.underside_alignment < MIN_UNDERSIDE_ALIGNMENT
159 || features.local_ball_position.x.abs() > MAX_LOCAL_FORWARD_OFFSET
160 || features.local_ball_position.y.abs() > MAX_LOCAL_LATERAL_OFFSET
161 || features.local_ball_position.z >= 10.0
162 {
163 return None;
164 }
165
166 let confidence = flip_reset_confidence(&features);
167 if confidence < MIN_CONFIDENCE {
168 return None;
169 }
170
171 Some(FlipResetHeuristic {
172 confidence,
173 local_ball_position: features.local_ball_position,
174 })
175}
176
177pub(crate) fn flip_reset_followup_touch_candidate(
181 ball_body: &boxcars::RigidBody,
182 player_body: &boxcars::RigidBody,
183 closest_approach_distance: f32,
184) -> Option<FlipResetHeuristic> {
185 let features = build_touch_features(ball_body, player_body, closest_approach_distance)?;
186 let confidence = flip_reset_confidence(&features);
187
188 if confidence < 0.45
189 || features.local_ball_position.z >= 20.0
190 || features.underside_alignment < 0.25
191 {
192 return None;
193 }
194
195 Some(FlipResetHeuristic {
196 confidence,
197 local_ball_position: features.local_ball_position,
198 })
199}
200
201fn flip_reset_proximity_candidate(
202 ball_body: &boxcars::RigidBody,
203 player_body: &boxcars::RigidBody,
204) -> Option<FlipResetHeuristic> {
205 const MIN_PLAYER_HEIGHT: f32 = 95.0;
206 const MIN_BALL_HEIGHT: f32 = 80.0;
207 const MAX_CENTER_DISTANCE: f32 = 110.0;
208 const MIN_UNDERSIDE_ALIGNMENT: f32 = 0.52;
209 const MAX_LOCAL_FORWARD_OFFSET: f32 = 260.0;
210 const MAX_LOCAL_LATERAL_OFFSET: f32 = 260.0;
211 const MIN_CONFIDENCE: f32 = 0.52;
212
213 let raw_ball_position = vec_to_glam(&ball_body.location);
214 let raw_player_position = vec_to_glam(&player_body.location);
215 let scale_factor = scale_factor_for_positions(raw_ball_position, raw_player_position);
216 let center_distance = (raw_ball_position - raw_player_position).length() * scale_factor;
217 let features = build_touch_features(ball_body, player_body, center_distance / scale_factor)?;
218 if features.player_position.z < MIN_PLAYER_HEIGHT || features.ball_position.z < MIN_BALL_HEIGHT
219 {
220 return None;
221 }
222 if features.center_distance > MAX_CENTER_DISTANCE
223 || features.underside_alignment < MIN_UNDERSIDE_ALIGNMENT
224 || features.local_ball_position.x.abs() > MAX_LOCAL_FORWARD_OFFSET
225 || features.local_ball_position.y.abs() > MAX_LOCAL_LATERAL_OFFSET
226 || features.local_ball_position.z >= 15.0
227 {
228 return None;
229 }
230
231 let confidence = flip_reset_confidence(&features);
232 if confidence < MIN_CONFIDENCE {
233 return None;
234 }
235
236 Some(FlipResetHeuristic {
237 confidence,
238 local_ball_position: features.local_ball_position,
239 })
240}
241
242#[derive(Debug, Clone, Default)]
243pub struct FlipResetTracker {
244 flip_reset_events: Vec<FlipResetEvent>,
245 current_frame_flip_reset_events: Vec<FlipResetEvent>,
246 post_wall_dodge_events: Vec<PostWallDodgeEvent>,
247 current_frame_post_wall_dodge_events: Vec<PostWallDodgeEvent>,
248 flip_reset_followup_dodge_events: Vec<FlipResetFollowupDodgeEvent>,
249 current_frame_flip_reset_followup_dodge_events: Vec<FlipResetFollowupDodgeEvent>,
250 recent_wall_contact_time: HashMap<PlayerId, f32>,
251 recent_flip_reset_candidates: HashMap<PlayerId, FlipResetEvent>,
252 recent_flip_reset_proximity_event_time: HashMap<PlayerId, f32>,
253 current_frame_dodge_rising_edges: Vec<PlayerId>,
254 previous_dodge_active: HashMap<PlayerId, bool>,
255}
256
257impl FlipResetTracker {
258 pub fn new() -> Self {
259 Self::default()
260 }
261
262 pub fn on_frame(
263 &mut self,
264 processor: &ReplayProcessor,
265 frame: &boxcars::Frame,
266 frame_index: usize,
267 ) -> SubtrActorResult<()> {
268 self.update_flip_reset_events(processor, frame.time, frame_index)?;
269 self.update_dodge_rising_edges(processor)?;
270 self.update_flip_reset_followup_dodge_events(processor, frame, frame_index)?;
271 self.update_post_wall_dodge_events(processor, frame, frame_index)?;
272 Ok(())
273 }
274
275 pub fn flip_reset_events(&self) -> &[FlipResetEvent] {
276 &self.flip_reset_events
277 }
278
279 pub fn current_frame_flip_reset_events(&self) -> &[FlipResetEvent] {
280 &self.current_frame_flip_reset_events
281 }
282
283 pub fn post_wall_dodge_events(&self) -> &[PostWallDodgeEvent] {
284 &self.post_wall_dodge_events
285 }
286
287 pub fn current_frame_post_wall_dodge_events(&self) -> &[PostWallDodgeEvent] {
288 &self.current_frame_post_wall_dodge_events
289 }
290
291 pub fn flip_reset_followup_dodge_events(&self) -> &[FlipResetFollowupDodgeEvent] {
292 &self.flip_reset_followup_dodge_events
293 }
294
295 pub fn current_frame_flip_reset_followup_dodge_events(&self) -> &[FlipResetFollowupDodgeEvent] {
296 &self.current_frame_flip_reset_followup_dodge_events
297 }
298
299 pub fn into_events(
300 self,
301 ) -> (
302 Vec<FlipResetEvent>,
303 Vec<PostWallDodgeEvent>,
304 Vec<FlipResetFollowupDodgeEvent>,
305 ) {
306 (
307 self.flip_reset_events,
308 self.post_wall_dodge_events,
309 self.flip_reset_followup_dodge_events,
310 )
311 }
312
313 fn build_flip_reset_event(
314 &self,
315 processor: &ReplayProcessor,
316 touch_event: &TouchEvent,
317 frame_index: usize,
318 ) -> Option<FlipResetEvent> {
319 let player = touch_event.player.as_ref()?;
320 let closest_approach_distance = touch_event.closest_approach_distance?;
321 let ball_rigid_body = processor.get_normalized_ball_rigid_body().ok()?;
322 let player_rigid_body = processor.get_normalized_player_rigid_body(player).ok()?;
323 let heuristic = flip_reset_candidate(
324 &ball_rigid_body,
325 &player_rigid_body,
326 closest_approach_distance,
327 )?;
328
329 Some(FlipResetEvent {
330 time: touch_event.time,
331 frame: frame_index,
332 player: player.clone(),
333 is_team_0: touch_event.team_is_team_0,
334 confidence: heuristic.confidence,
335 local_ball_position: glam_to_vec(&heuristic.local_ball_position),
336 closest_approach_distance,
337 })
338 }
339
340 fn build_flip_reset_event_for_player(
341 &self,
342 processor: &ReplayProcessor,
343 player: &PlayerId,
344 time: f32,
345 frame_index: usize,
346 is_team_0: bool,
347 closest_approach_distance: f32,
348 ) -> Option<FlipResetEvent> {
349 let ball_rigid_body = processor.get_normalized_ball_rigid_body().ok()?;
350 let player_rigid_body = processor.get_normalized_player_rigid_body(player).ok()?;
351 let heuristic = flip_reset_candidate(
352 &ball_rigid_body,
353 &player_rigid_body,
354 closest_approach_distance,
355 )?;
356
357 Some(FlipResetEvent {
358 time,
359 frame: frame_index,
360 player: player.clone(),
361 is_team_0,
362 confidence: heuristic.confidence,
363 local_ball_position: glam_to_vec(&heuristic.local_ball_position),
364 closest_approach_distance,
365 })
366 }
367
368 fn build_flip_reset_followup_touch_candidate(
369 &self,
370 processor: &ReplayProcessor,
371 touch_event: &TouchEvent,
372 frame_index: usize,
373 ) -> Option<FlipResetEvent> {
374 let player = touch_event.player.as_ref()?;
375 let closest_approach_distance = touch_event.closest_approach_distance?;
376 let ball_rigid_body = processor.get_normalized_ball_rigid_body().ok()?;
377 let player_rigid_body = processor.get_normalized_player_rigid_body(player).ok()?;
378 let heuristic = flip_reset_followup_touch_candidate(
379 &ball_rigid_body,
380 &player_rigid_body,
381 closest_approach_distance,
382 )?;
383
384 Some(FlipResetEvent {
385 time: touch_event.time,
386 frame: frame_index,
387 player: player.clone(),
388 is_team_0: touch_event.team_is_team_0,
389 confidence: heuristic.confidence,
390 local_ball_position: glam_to_vec(&heuristic.local_ball_position),
391 closest_approach_distance,
392 })
393 }
394
395 fn build_flip_reset_proximity_event(
396 &self,
397 processor: &ReplayProcessor,
398 player: &PlayerId,
399 time: f32,
400 frame_index: usize,
401 ) -> Option<FlipResetEvent> {
402 let ball_rigid_body = processor.get_normalized_ball_rigid_body().ok()?;
403 let player_rigid_body = processor.get_normalized_player_rigid_body(player).ok()?;
404 let heuristic = flip_reset_proximity_candidate(&ball_rigid_body, &player_rigid_body)?;
405 let raw_ball_position = vec_to_glam(&ball_rigid_body.location);
406 let raw_player_position = vec_to_glam(&player_rigid_body.location);
407 let scale_factor = scale_factor_for_positions(raw_ball_position, raw_player_position);
408 let closest_approach_distance =
409 (raw_ball_position - raw_player_position).length() * scale_factor;
410
411 Some(FlipResetEvent {
412 time,
413 frame: frame_index,
414 player: player.clone(),
415 is_team_0: processor.get_player_is_team_0(player).unwrap_or(false),
416 confidence: heuristic.confidence,
417 local_ball_position: glam_to_vec(&heuristic.local_ball_position),
418 closest_approach_distance,
419 })
420 }
421
422 fn update_flip_reset_events(
423 &mut self,
424 processor: &ReplayProcessor,
425 current_time: f32,
426 frame_index: usize,
427 ) -> SubtrActorResult<()> {
428 const PROXIMITY_EVENT_DEBOUNCE_SECONDS: f32 = 0.35;
429
430 self.current_frame_flip_reset_events.clear();
431 for touch_event in processor.current_frame_touch_events() {
432 let event = self
433 .build_flip_reset_event(processor, touch_event, frame_index)
434 .or_else(|| {
435 let ball_rigid_body = processor.get_normalized_ball_rigid_body().ok()?;
436 let ball_position = vec_to_glam(&ball_rigid_body.location);
437 processor
438 .iter_player_ids_in_order()
439 .filter(|player| {
440 processor.get_player_is_team_0(player).ok()
441 == Some(touch_event.team_is_team_0)
442 })
443 .filter_map(|player| {
444 let player_rigid_body =
445 processor.get_normalized_player_rigid_body(player).ok()?;
446 let player_position = vec_to_glam(&player_rigid_body.location);
447 let fallback_touch_distance =
448 (ball_position - player_position).length();
449 self.build_flip_reset_event_for_player(
450 processor,
451 player,
452 touch_event.time,
453 frame_index,
454 touch_event.team_is_team_0,
455 fallback_touch_distance,
456 )
457 })
458 .max_by(|left, right| {
459 left.confidence
460 .partial_cmp(&right.confidence)
461 .unwrap_or(std::cmp::Ordering::Equal)
462 })
463 });
464 let Some(event) = event else {
465 continue;
466 };
467 self.current_frame_flip_reset_events.push(event.clone());
468 self.flip_reset_events.push(event);
469 }
470
471 for player in processor.iter_player_ids_in_order() {
472 let already_emitted_this_frame = self
473 .current_frame_flip_reset_events
474 .iter()
475 .any(|event| &event.player == player);
476 if already_emitted_this_frame {
477 continue;
478 }
479 if self
480 .recent_flip_reset_proximity_event_time
481 .get(player)
482 .map(|previous_time| {
483 current_time - previous_time < PROXIMITY_EVENT_DEBOUNCE_SECONDS
484 })
485 .unwrap_or(false)
486 {
487 continue;
488 }
489 let Some(event) =
490 self.build_flip_reset_proximity_event(processor, player, current_time, frame_index)
491 else {
492 continue;
493 };
494 self.recent_flip_reset_proximity_event_time
495 .insert(player.clone(), current_time);
496 self.current_frame_flip_reset_events.push(event.clone());
497 self.flip_reset_events.push(event);
498 }
499 Ok(())
500 }
501
502 fn update_dodge_rising_edges(&mut self, processor: &ReplayProcessor) -> SubtrActorResult<()> {
503 self.current_frame_dodge_rising_edges.clear();
504 let player_ids: Vec<_> = processor.iter_player_ids_in_order().cloned().collect();
505
506 for player_id in player_ids {
507 let dodge_active = processor.get_dodge_active(&player_id).unwrap_or(0) % 2 == 1;
508 let was_dodge_active = self
509 .previous_dodge_active
510 .insert(player_id.clone(), dodge_active)
511 .unwrap_or(false);
512 if dodge_active && !was_dodge_active {
513 self.current_frame_dodge_rising_edges.push(player_id);
514 }
515 }
516
517 Ok(())
518 }
519
520 fn wall_sequence_scale_factor(player_rigid_body: &boxcars::RigidBody) -> f32 {
521 if player_rigid_body
522 .location
523 .x
524 .abs()
525 .max(player_rigid_body.location.y.abs())
526 < 200.0
527 {
528 100.0
529 } else {
530 1.0
531 }
532 }
533
534 fn player_is_grounded_for_wall_sequence(player_rigid_body: &boxcars::RigidBody) -> bool {
535 player_rigid_body.location.z * Self::wall_sequence_scale_factor(player_rigid_body) <= 80.0
536 }
537
538 fn player_is_touching_wall(player_rigid_body: &boxcars::RigidBody) -> bool {
539 let scale_factor = Self::wall_sequence_scale_factor(player_rigid_body);
540 let location = &player_rigid_body.location;
541 let x = location.x.abs() * scale_factor;
542 let y = location.y.abs() * scale_factor;
543 let z = location.z * scale_factor;
544 z >= 120.0 && (x >= 3600.0 || y >= 5000.0)
545 }
546
547 fn update_post_wall_dodge_events(
548 &mut self,
549 processor: &ReplayProcessor,
550 frame: &boxcars::Frame,
551 frame_index: usize,
552 ) -> SubtrActorResult<()> {
553 const MIN_DELAY_AFTER_WALL_SECONDS: f32 = 0.20;
554 const MAX_DELAY_AFTER_WALL_SECONDS: f32 = 1.10;
555
556 self.current_frame_post_wall_dodge_events.clear();
557 let current_time = frame.time;
558 let player_ids: Vec<_> = processor.iter_player_ids_in_order().cloned().collect();
559
560 for player_id in &player_ids {
561 let Ok(player_rigid_body) = processor.get_normalized_player_rigid_body(player_id)
562 else {
563 self.previous_dodge_active.remove(player_id);
564 continue;
565 };
566
567 let is_grounded = Self::player_is_grounded_for_wall_sequence(&player_rigid_body);
568 if is_grounded {
569 self.recent_wall_contact_time.remove(player_id);
570 } else if Self::player_is_touching_wall(&player_rigid_body) {
571 self.recent_wall_contact_time
572 .insert(player_id.clone(), current_time);
573 }
574 }
575
576 for player_id in &self.current_frame_dodge_rising_edges {
577 let Ok(player_rigid_body) = processor.get_normalized_player_rigid_body(player_id)
578 else {
579 continue;
580 };
581 let is_grounded = Self::player_is_grounded_for_wall_sequence(&player_rigid_body);
582 if is_grounded {
583 continue;
584 }
585
586 let Some(wall_contact_time) = self.recent_wall_contact_time.get(player_id).copied()
587 else {
588 continue;
589 };
590 let time_since_wall_contact = current_time - wall_contact_time;
591 if !(MIN_DELAY_AFTER_WALL_SECONDS..=MAX_DELAY_AFTER_WALL_SECONDS)
592 .contains(&time_since_wall_contact)
593 {
594 continue;
595 }
596 if Self::player_is_touching_wall(&player_rigid_body) {
597 continue;
598 }
599
600 let event = PostWallDodgeEvent {
601 time: current_time,
602 frame: frame_index,
603 player: player_id.clone(),
604 is_team_0: processor.get_player_is_team_0(player_id).unwrap_or(false),
605 wall_contact_time,
606 time_since_wall_contact,
607 };
608 self.current_frame_post_wall_dodge_events
609 .push(event.clone());
610 self.post_wall_dodge_events.push(event);
611 }
612
613 Ok(())
614 }
615
616 fn update_flip_reset_followup_dodge_events(
617 &mut self,
618 processor: &ReplayProcessor,
619 frame: &boxcars::Frame,
620 frame_index: usize,
621 ) -> SubtrActorResult<()> {
622 const MIN_DELAY_AFTER_CANDIDATE_TOUCH_SECONDS: f32 = 0.05;
623 const MAX_DELAY_AFTER_CANDIDATE_TOUCH_SECONDS: f32 = 1.75;
624
625 self.current_frame_flip_reset_followup_dodge_events.clear();
626 let current_time = frame.time;
627 let player_ids: Vec<_> = processor.iter_player_ids_in_order().cloned().collect();
628
629 for player_id in &player_ids {
630 let Ok(player_rigid_body) = processor.get_normalized_player_rigid_body(player_id)
631 else {
632 self.recent_flip_reset_candidates.remove(player_id);
633 continue;
634 };
635 if Self::player_is_grounded_for_wall_sequence(&player_rigid_body) {
636 self.recent_flip_reset_candidates.remove(player_id);
637 }
638 }
639
640 for touch_event in processor.current_frame_touch_events() {
641 let Some(event) =
642 self.build_flip_reset_followup_touch_candidate(processor, touch_event, frame_index)
643 else {
644 continue;
645 };
646 self.recent_flip_reset_candidates
647 .insert(event.player.clone(), event.clone());
648 }
649
650 for player_id in &self.current_frame_dodge_rising_edges {
651 let Ok(player_rigid_body) = processor.get_normalized_player_rigid_body(player_id)
652 else {
653 continue;
654 };
655 if Self::player_is_grounded_for_wall_sequence(&player_rigid_body) {
656 continue;
657 }
658
659 let Some(candidate_event) = self.recent_flip_reset_candidates.get(player_id).cloned()
660 else {
661 continue;
662 };
663 let time_since_candidate_touch = current_time - candidate_event.time;
664 if !(MIN_DELAY_AFTER_CANDIDATE_TOUCH_SECONDS..=MAX_DELAY_AFTER_CANDIDATE_TOUCH_SECONDS)
665 .contains(&time_since_candidate_touch)
666 {
667 continue;
668 }
669
670 let event = FlipResetFollowupDodgeEvent {
671 time: current_time,
672 frame: frame_index,
673 player: player_id.clone(),
674 is_team_0: processor.get_player_is_team_0(player_id).unwrap_or(false),
675 candidate_touch_time: candidate_event.time,
676 time_since_candidate_touch,
677 candidate_touch_confidence: candidate_event.confidence,
678 };
679 self.current_frame_flip_reset_followup_dodge_events
680 .push(event.clone());
681 self.flip_reset_followup_dodge_events.push(event);
682 self.recent_flip_reset_candidates.remove(player_id);
683 }
684
685 Ok(())
686 }
687}
688
689impl Collector for FlipResetTracker {
690 fn process_frame(
691 &mut self,
692 processor: &ReplayProcessor,
693 frame: &boxcars::Frame,
694 frame_number: usize,
695 _current_time: f32,
696 ) -> SubtrActorResult<TimeAdvance> {
697 self.on_frame(processor, frame, frame_number)?;
698 Ok(TimeAdvance::NextFrame)
699 }
700}