subtr_actor/stats/calculators/
dodge_reset.rs1use super::*;
2
3const FLIP_RESET_MIN_DODGE_TOUCH_DELAY_SECONDS: f32 = 0.05;
4const FLIP_RESET_MAX_DODGE_TOUCH_DELAY_SECONDS: f32 = 2.0;
5const FLIP_RESET_GROUNDED_Z: f32 = 80.0;
6
7#[derive(Debug, Clone, PartialEq, Serialize, ts_rs::TS)]
8#[ts(export)]
9pub struct DodgeResetEvent {
10 pub time: f32,
11 pub frame: usize,
12 #[ts(as = "crate::ts_bindings::RemoteIdTs")]
13 pub player: PlayerId,
14 pub is_team_0: bool,
15 pub counter_value: i32,
16 pub on_ball: bool,
17}
18
19#[derive(Debug, Clone, PartialEq, Serialize)]
20pub struct ConfirmedFlipResetEvent {
21 pub time: f32,
22 pub frame: usize,
23 pub reset_time: f32,
24 pub reset_frame: usize,
25 pub player: PlayerId,
26 pub is_team_0: bool,
27 pub counter_value: i32,
28 pub time_since_reset: f32,
29}
30
31#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, ts_rs::TS)]
32#[ts(export)]
33pub struct DodgeResetStats {
34 pub count: u32,
35 pub on_ball_count: u32,
36}
37
38#[derive(Debug, Clone, Default, PartialEq)]
39pub struct DodgeResetCalculator {
40 player_stats: HashMap<PlayerId, DodgeResetStats>,
41 events: Vec<DodgeResetEvent>,
42 on_ball_events: Vec<DodgeRefreshedEvent>,
43 confirmed_flip_reset_events: Vec<ConfirmedFlipResetEvent>,
44 pending_on_ball_resets: HashMap<PlayerId, DodgeRefreshedEvent>,
45 pending_reset_dodge_started: HashSet<PlayerId>,
46 previous_dodge_active: HashMap<PlayerId, bool>,
47}
48
49impl DodgeResetCalculator {
50 pub fn new() -> Self {
51 Self::default()
52 }
53
54 pub fn player_stats(&self) -> &HashMap<PlayerId, DodgeResetStats> {
55 &self.player_stats
56 }
57
58 pub fn events(&self) -> &[DodgeResetEvent] {
59 &self.events
60 }
61
62 pub fn on_ball_events(&self) -> &[DodgeRefreshedEvent] {
63 &self.on_ball_events
64 }
65
66 pub fn confirmed_flip_reset_events(&self) -> &[ConfirmedFlipResetEvent] {
67 &self.confirmed_flip_reset_events
68 }
69
70 fn player<'a>(players: &'a PlayerFrameState, player_id: &PlayerId) -> Option<&'a PlayerSample> {
71 players
72 .players
73 .iter()
74 .find(|player| &player.player_id == player_id)
75 }
76
77 fn player_is_grounded(players: &PlayerFrameState, player_id: &PlayerId) -> bool {
78 Self::player(players, player_id)
79 .and_then(PlayerSample::position)
80 .is_some_and(|position| position.z <= FLIP_RESET_GROUNDED_Z)
81 }
82
83 fn player_dodge_active(players: &PlayerFrameState, player_id: &PlayerId) -> bool {
84 Self::player(players, player_id).is_some_and(|player| player.dodge_active)
85 }
86
87 fn on_ball_dodge_reset(
88 ball: &BallFrameState,
89 players: &PlayerFrameState,
90 player_id: &PlayerId,
91 ) -> bool {
92 const MIN_PLAYER_HEIGHT: f32 = 95.0;
93 const MIN_BALL_HEIGHT: f32 = 80.0;
94 const MAX_CENTER_DISTANCE: f32 = 180.0;
95 const MAX_LOCAL_VERTICAL_OFFSET: f32 = 140.0;
96
97 let Some(ball) = ball.sample() else {
98 return false;
99 };
100 let Some(player) = Self::player(players, player_id) else {
101 return false;
102 };
103 let Some(player_rigid_body) = &player.rigid_body else {
104 return false;
105 };
106
107 let ball_position = vec_to_glam(&ball.rigid_body.location);
108 let player_position = vec_to_glam(&player_rigid_body.location);
109 if player_position.z < MIN_PLAYER_HEIGHT || ball_position.z < MIN_BALL_HEIGHT {
110 return false;
111 }
112
113 let relative_ball_position = ball_position - player_position;
114 let center_distance = relative_ball_position.length();
115 if !center_distance.is_finite() || center_distance > MAX_CENTER_DISTANCE {
116 return false;
117 }
118
119 let player_rotation = quat_to_glam(&player_rigid_body.rotation);
120 let local_ball_position = player_rotation.inverse() * relative_ball_position;
121 local_ball_position.z <= MAX_LOCAL_VERTICAL_OFFSET
122 }
123
124 fn prune_pending_resets(&mut self, players: &PlayerFrameState) {
125 let grounded_players = self
126 .pending_on_ball_resets
127 .keys()
128 .filter(|player_id| Self::player_is_grounded(players, player_id))
129 .cloned()
130 .collect::<Vec<_>>();
131 for player_id in grounded_players {
132 self.pending_on_ball_resets.remove(&player_id);
133 self.pending_reset_dodge_started.remove(&player_id);
134 }
135 }
136
137 fn update_pending_reset_dodges(&mut self, players: &PlayerFrameState) {
138 for player in &players.players {
139 let was_dodge_active = self
140 .previous_dodge_active
141 .insert(player.player_id.clone(), player.dodge_active)
142 .unwrap_or(false);
143 if player.dodge_active
144 && !was_dodge_active
145 && self.pending_on_ball_resets.contains_key(&player.player_id)
146 {
147 self.pending_reset_dodge_started
148 .insert(player.player_id.clone());
149 }
150 }
151 }
152
153 fn apply_confirmed_flip_reset_touch(
154 &mut self,
155 players: &PlayerFrameState,
156 touch_event: &TouchEvent,
157 ) {
158 let Some(player_id) = touch_event.player.as_ref() else {
159 return;
160 };
161 if !self.pending_reset_dodge_started.contains(player_id)
162 || !Self::player_dodge_active(players, player_id)
163 {
164 return;
165 }
166
167 let Some(reset_event) = self.pending_on_ball_resets.get(player_id).cloned() else {
168 return;
169 };
170 let time_since_reset = touch_event.time - reset_event.time;
171 if !(FLIP_RESET_MIN_DODGE_TOUCH_DELAY_SECONDS..=FLIP_RESET_MAX_DODGE_TOUCH_DELAY_SECONDS)
172 .contains(&time_since_reset)
173 {
174 if time_since_reset > FLIP_RESET_MAX_DODGE_TOUCH_DELAY_SECONDS {
175 self.pending_on_ball_resets.remove(player_id);
176 self.pending_reset_dodge_started.remove(player_id);
177 }
178 return;
179 }
180
181 self.confirmed_flip_reset_events
182 .push(ConfirmedFlipResetEvent {
183 time: touch_event.time,
184 frame: touch_event.frame,
185 reset_time: reset_event.time,
186 reset_frame: reset_event.frame,
187 player: player_id.clone(),
188 is_team_0: touch_event.team_is_team_0,
189 counter_value: reset_event.counter_value,
190 time_since_reset,
191 });
192 self.pending_on_ball_resets.remove(player_id);
193 self.pending_reset_dodge_started.remove(player_id);
194 }
195
196 pub fn update(
197 &mut self,
198 ball: &BallFrameState,
199 players: &PlayerFrameState,
200 events: &FrameEventsState,
201 ) -> SubtrActorResult<()> {
202 self.prune_pending_resets(players);
203 for event in &events.dodge_refreshed_events {
204 let on_ball = Self::on_ball_dodge_reset(ball, players, &event.player);
205 let stats = self.player_stats.entry(event.player.clone()).or_default();
206 stats.count += 1;
207 if on_ball {
208 stats.on_ball_count += 1;
209 self.on_ball_events.push(event.clone());
210 self.pending_on_ball_resets
211 .insert(event.player.clone(), event.clone());
212 self.pending_reset_dodge_started.remove(&event.player);
213 }
214 self.events.push(DodgeResetEvent {
215 time: event.time,
216 frame: event.frame,
217 player: event.player.clone(),
218 is_team_0: event.is_team_0,
219 counter_value: event.counter_value,
220 on_ball,
221 });
222 }
223 self.update_pending_reset_dodges(players);
224 for touch_event in &events.touch_events {
225 self.apply_confirmed_flip_reset_touch(players, touch_event);
226 }
227 Ok(())
228 }
229}
230
231#[cfg(test)]
232#[path = "dodge_reset_tests.rs"]
233mod tests;