Skip to main content

subtr_actor/stats/calculators/
dodge_reset.rs

1use 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;