subtr_actor/stats/calculators/
movement.rs1use super::*;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4enum MovementSpeedBand {
5 Slow,
6 Boost,
7 Supersonic,
8}
9
10impl MovementSpeedBand {
11 fn as_label_value(self) -> &'static str {
12 match self {
13 Self::Slow => "slow",
14 Self::Boost => "boost",
15 Self::Supersonic => "supersonic",
16 }
17 }
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21struct MovementClassification {
22 speed_band: MovementSpeedBand,
23 height_band: PlayerVerticalBand,
24}
25
26#[derive(Debug, Clone, PartialEq, Serialize, ts_rs::TS)]
28#[ts(export)]
29pub struct MovementEvent {
30 pub time: f32,
31 pub frame: usize,
32 pub end_time: f32,
33 pub end_frame: usize,
34 #[ts(as = "crate::interop::ts_bindings::RemoteIdTs")]
35 pub player: PlayerId,
36 #[serde(default, skip_serializing_if = "Option::is_none")]
37 pub player_position: Option<[f32; 3]>,
38 pub is_team_0: bool,
39 pub dt: f32,
40 pub speed: f32,
41 pub distance: f32,
42 pub speed_band: String,
43 pub height_band: String,
44}
45
46impl MovementEvent {
47 fn absorb_sample(&mut self, sample: Self) {
48 self.end_time = sample.time;
49 self.end_frame = sample.frame;
50 self.player_position = sample.player_position;
51 let combined_dt = self.dt + sample.dt;
52 if combined_dt > 0.0 {
53 self.speed = (self.speed * self.dt + sample.speed * sample.dt) / combined_dt;
54 }
55 self.dt = combined_dt;
56 self.distance += sample.distance;
57 }
58}
59
60#[derive(Debug, Clone, Default)]
62pub struct MovementCalculator {
63 player_teams: HashMap<PlayerId, bool>,
64 previous_positions: HashMap<PlayerId, glam::Vec3>,
65 events: EventStream<MovementEvent>,
66 pending_events: HashMap<PlayerId, PendingMovementEvent>,
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70struct MovementEventState {
71 is_team_0: bool,
72 classification: MovementClassification,
73}
74
75#[derive(Debug, Clone, PartialEq)]
76struct PendingMovementEvent {
77 state: MovementEventState,
78 event: MovementEvent,
79}
80
81impl MovementCalculator {
82 pub fn new() -> Self {
83 Self::default()
84 }
85
86 pub fn events(&self) -> &[MovementEvent] {
87 self.events.all()
88 }
89
90 pub fn new_events(&self) -> &[MovementEvent] {
91 self.events.new_events()
92 }
93
94 pub fn pending_events(&self) -> Vec<MovementEvent> {
102 let mut pending: Vec<_> = self
103 .pending_events
104 .values()
105 .map(|pending| pending.event.clone())
106 .collect();
107 pending.sort_by(|left, right| {
108 left.frame
109 .cmp(&right.frame)
110 .then_with(|| format!("{:?}", left.player).cmp(&format!("{:?}", right.player)))
111 });
112 pending
113 }
114
115 pub fn projected_events(&self) -> Vec<MovementEvent> {
116 let mut events = self.events.all().to_vec();
117 events.extend(self.pending_events());
118 events
119 }
120
121 pub fn flush_pending_events(&mut self) {
122 let mut pending: Vec<_> = self
123 .pending_events
124 .drain()
125 .map(|(_, pending)| pending)
126 .collect();
127 pending.sort_by(|left, right| {
128 left.event.frame.cmp(&right.event.frame).then_with(|| {
129 format!("{:?}", left.event.player).cmp(&format!("{:?}", right.event.player))
130 })
131 });
132 self.events
133 .extend(pending.into_iter().map(|pending| pending.event));
134 }
135
136 fn classify_movement(speed: f32, height_band: PlayerVerticalBand) -> MovementClassification {
137 let speed_band = if speed >= SUPERSONIC_SPEED_THRESHOLD {
138 MovementSpeedBand::Supersonic
139 } else if speed >= BOOST_SPEED_THRESHOLD {
140 MovementSpeedBand::Boost
141 } else {
142 MovementSpeedBand::Slow
143 };
144
145 MovementClassification {
146 speed_band,
147 height_band,
148 }
149 }
150
151 pub fn update(
152 &mut self,
153 frame: &FrameInfo,
154 players: &PlayerFrameState,
155 vertical_state: &PlayerVerticalState,
156 live_play_state: &LivePlayState,
157 ) -> SubtrActorResult<()> {
158 self.events.begin_update();
159 if !live_play_state.counts_toward_player_motion() {
160 self.flush_pending_events();
161 for player in &players.players {
162 if let Some(position) = player.position() {
163 self.previous_positions
164 .insert(player.player_id.clone(), position);
165 }
166 }
167 return Ok(());
168 }
169
170 let active_players: HashSet<_> = players
171 .players
172 .iter()
173 .map(|player| player.player_id.clone())
174 .collect();
175 self.flush_pending_events_for_missing_players(&active_players);
176
177 if frame.dt == 0.0 {
178 for player in &players.players {
179 if let Some(position) = player.position() {
180 self.previous_positions
181 .insert(player.player_id.clone(), position);
182 }
183 }
184 return Ok(());
185 }
186
187 for player in &players.players {
188 self.player_teams
189 .insert(player.player_id.clone(), player.is_team_0);
190 let Some(position) = player.position() else {
191 self.flush_pending_event_for_player(&player.player_id);
192 continue;
193 };
194 let speed = player.speed().unwrap_or(0.0);
195 let distance =
196 if let Some(previous_position) = self.previous_positions.get(&player.player_id) {
197 position.distance(*previous_position)
198 } else {
199 0.0
200 };
201
202 let height_band = vertical_state
203 .band_for_player(&player.player_id)
204 .unwrap_or_else(|| PlayerVerticalBand::from_height(position.z));
205 let classification = Self::classify_movement(speed, height_band);
206 let event = MovementEvent {
207 time: frame.time,
208 frame: frame.frame_number,
209 end_time: frame.time,
210 end_frame: frame.frame_number,
211 player: player.player_id.clone(),
212 player_position: Some(position.to_array()),
213 is_team_0: player.is_team_0,
214 dt: frame.dt,
215 speed,
216 distance,
217 speed_band: classification.speed_band.as_label_value().to_owned(),
218 height_band: classification.height_band.as_label().value.to_owned(),
219 };
220 self.record_event(
221 MovementEventState {
222 is_team_0: player.is_team_0,
223 classification,
224 },
225 event,
226 );
227
228 self.previous_positions
229 .insert(player.player_id.clone(), position);
230 }
231
232 Ok(())
233 }
234
235 fn record_event(&mut self, state: MovementEventState, event: MovementEvent) {
236 let player = event.player.clone();
237 let Some(pending) = self.pending_events.get_mut(&player) else {
238 self.pending_events
239 .insert(player, PendingMovementEvent { state, event });
240 return;
241 };
242
243 if pending.state == state {
244 pending.event.absorb_sample(event);
245 } else {
246 let previous = self
247 .pending_events
248 .insert(player, PendingMovementEvent { state, event });
249 let Some(previous) = previous else {
250 return;
251 };
252 self.events.push(previous.event);
253 }
254 }
255
256 fn flush_pending_event_for_player(&mut self, player_id: &PlayerId) {
257 let Some(pending) = self.pending_events.remove(player_id) else {
258 return;
259 };
260 self.events.push(pending.event);
261 }
262
263 fn flush_pending_events_for_missing_players(&mut self, active_players: &HashSet<PlayerId>) {
264 let missing_players: Vec<_> = self
265 .pending_events
266 .keys()
267 .filter(|player_id| !active_players.contains(*player_id))
268 .cloned()
269 .collect();
270 for player_id in missing_players {
271 self.flush_pending_event_for_player(&player_id);
272 }
273 }
274}