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