Skip to main content

subtr_actor/stats/calculators/
demo.rs

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