subtr_actor/stats/calculators/
demo.rs1use super::*;
2
3const DEMO_REPEAT_FRAME_WINDOW: usize = 8;
4
5#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, ts_rs::TS)]
6#[ts(export)]
7pub struct DemoPlayerStats {
8 pub demos_inflicted: u32,
9 pub demos_taken: u32,
10}
11
12#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, ts_rs::TS)]
13#[ts(export)]
14pub struct DemoTeamStats {
15 pub demos_inflicted: u32,
16}
17
18#[derive(Debug, Clone, Default, PartialEq)]
19pub struct DemoCalculator {
20 player_stats: HashMap<PlayerId, DemoPlayerStats>,
21 player_teams: HashMap<PlayerId, bool>,
22 team_zero_stats: DemoTeamStats,
23 team_one_stats: DemoTeamStats,
24 timeline: Vec<TimelineEvent>,
25 last_seen_frame: HashMap<(PlayerId, PlayerId), usize>,
26}
27
28impl DemoCalculator {
29 pub fn new() -> Self {
30 Self::default()
31 }
32
33 pub fn player_stats(&self) -> &HashMap<PlayerId, DemoPlayerStats> {
34 &self.player_stats
35 }
36
37 pub fn team_zero_stats(&self) -> &DemoTeamStats {
38 &self.team_zero_stats
39 }
40
41 pub fn team_one_stats(&self) -> &DemoTeamStats {
42 &self.team_one_stats
43 }
44
45 pub fn timeline(&self) -> &[TimelineEvent] {
46 &self.timeline
47 }
48
49 fn should_count_demo(
50 &mut self,
51 attacker: &PlayerId,
52 victim: &PlayerId,
53 frame_number: usize,
54 ) -> bool {
55 let key = (attacker.clone(), victim.clone());
56 let already_counted = self
57 .last_seen_frame
58 .get(&key)
59 .map(|previous_frame| {
60 frame_number.saturating_sub(*previous_frame) <= DEMO_REPEAT_FRAME_WINDOW
61 })
62 .unwrap_or(false);
63 self.last_seen_frame.insert(key, frame_number);
64 !already_counted
65 }
66
67 pub fn update(
68 &mut self,
69 frame: &FrameInfo,
70 players: &PlayerFrameState,
71 events: &FrameEventsState,
72 ) -> SubtrActorResult<()> {
73 for player in &players.players {
74 self.player_teams
75 .insert(player.player_id.clone(), player.is_team_0);
76 }
77
78 if !events.demo_events.is_empty() {
79 for demo in &events.demo_events {
80 self.record_demo(&demo.attacker, &demo.victim, demo.time, demo.frame);
81 }
82 return Ok(());
83 }
84
85 for demo in &events.active_demos {
86 self.record_demo(&demo.attacker, &demo.victim, frame.time, frame.frame_number);
87 }
88
89 Ok(())
90 }
91}
92
93impl DemoCalculator {
94 fn record_demo(
95 &mut self,
96 attacker: &PlayerId,
97 victim: &PlayerId,
98 time: f32,
99 frame_number: usize,
100 ) {
101 if !self.should_count_demo(attacker, victim, frame_number) {
102 return;
103 }
104
105 self.player_stats
106 .entry(attacker.clone())
107 .or_default()
108 .demos_inflicted += 1;
109 self.player_stats
110 .entry(victim.clone())
111 .or_default()
112 .demos_taken += 1;
113
114 match self.player_teams.get(attacker).copied() {
115 Some(true) => self.team_zero_stats.demos_inflicted += 1,
116 Some(false) => self.team_one_stats.demos_inflicted += 1,
117 None => {}
118 }
119
120 self.timeline.push(TimelineEvent {
121 time,
122 kind: TimelineEventKind::Kill,
123 player_id: Some(attacker.clone()),
124 is_team_0: self.player_teams.get(attacker).copied(),
125 });
126 self.timeline.push(TimelineEvent {
127 time,
128 kind: TimelineEventKind::Death,
129 player_id: Some(victim.clone()),
130 is_team_0: self.player_teams.get(victim).copied(),
131 });
132 }
133}