subtr_actor/stats/calculators/
powerslide.rs1use 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}