Skip to main content

subtr_actor/stats/calculators/
powerslide.rs

1use super::*;
2
3#[derive(Debug, Clone, PartialEq, Serialize, ts_rs::TS)]
4#[ts(export)]
5pub struct PowerslideEvent {
6    pub time: f32,
7    pub frame: usize,
8    #[ts(as = "crate::ts_bindings::RemoteIdTs")]
9    pub player: PlayerId,
10    pub is_team_0: bool,
11    pub active: bool,
12}
13
14#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, ts_rs::TS)]
15#[ts(export)]
16pub struct PowerslideStats {
17    pub total_duration: f32,
18    pub press_count: u32,
19}
20
21impl PowerslideStats {
22    pub fn average_duration(&self) -> f32 {
23        if self.press_count == 0 {
24            0.0
25        } else {
26            self.total_duration / self.press_count as f32
27        }
28    }
29}
30
31#[derive(Debug, Clone, Default)]
32pub struct PowerslideCalculator {
33    player_stats: HashMap<PlayerId, PowerslideStats>,
34    team_zero_stats: PowerslideStats,
35    team_one_stats: PowerslideStats,
36    last_active: HashMap<PlayerId, bool>,
37    events: Vec<PowerslideEvent>,
38}
39
40impl PowerslideCalculator {
41    pub fn new() -> Self {
42        Self::default()
43    }
44
45    pub fn player_stats(&self) -> &HashMap<PlayerId, PowerslideStats> {
46        &self.player_stats
47    }
48
49    pub fn team_zero_stats(&self) -> &PowerslideStats {
50        &self.team_zero_stats
51    }
52
53    pub fn team_one_stats(&self) -> &PowerslideStats {
54        &self.team_one_stats
55    }
56
57    pub fn events(&self) -> &[PowerslideEvent] {
58        &self.events
59    }
60
61    fn is_effective_powerslide(player: &PlayerSample) -> bool {
62        player.powerslide_active
63            && player
64                .position()
65                .map(|position| position.z <= POWERSLIDE_MAX_Z_THRESHOLD)
66                .unwrap_or(false)
67    }
68
69    pub fn update(
70        &mut self,
71        frame: &FrameInfo,
72        players: &PlayerFrameState,
73        live_play: bool,
74    ) -> SubtrActorResult<()> {
75        for player in &players.players {
76            let effective_powerslide = Self::is_effective_powerslide(player);
77            let previous_active = self
78                .last_active
79                .get(&player.player_id)
80                .copied()
81                .unwrap_or(false);
82            let stats = self
83                .player_stats
84                .entry(player.player_id.clone())
85                .or_default();
86            let team_stats = if player.is_team_0 {
87                &mut self.team_zero_stats
88            } else {
89                &mut self.team_one_stats
90            };
91
92            if live_play && effective_powerslide {
93                stats.total_duration += frame.dt;
94                team_stats.total_duration += frame.dt;
95            }
96
97            if live_play && effective_powerslide && !previous_active {
98                stats.press_count += 1;
99                team_stats.press_count += 1;
100            }
101
102            if effective_powerslide != previous_active {
103                self.events.push(PowerslideEvent {
104                    time: frame.time,
105                    frame: frame.frame_number,
106                    player: player.player_id.clone(),
107                    is_team_0: player.is_team_0,
108                    active: effective_powerslide,
109                });
110            }
111
112            self.last_active
113                .insert(player.player_id.clone(), effective_powerslide);
114        }
115        Ok(())
116    }
117}