1use std::collections::HashSet;
2
3use serde::{Deserialize, Serialize};
4
5use super::*;
6
7const DEFAULT_RUSH_MAX_START_Y: f32 = -BOOST_PAD_MIDFIELD_TOLERANCE_Y;
10const DEFAULT_RUSH_ATTACK_SUPPORT_DISTANCE_Y: f32 = 900.0;
11const DEFAULT_RUSH_DEFENDER_DISTANCE_Y: f32 = 150.0;
12const DEFAULT_RUSH_MIN_POSSESSION_RETAINED_SECONDS: f32 = 0.75;
13
14#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ts_rs::TS)]
15#[ts(export)]
16pub struct RushEvent {
17 pub start_time: f32,
18 pub start_frame: usize,
19 pub end_time: f32,
20 pub end_frame: usize,
21 pub is_team_0: bool,
22 pub attackers: usize,
23 pub defenders: usize,
24}
25
26#[derive(Debug, Clone, PartialEq)]
27struct ActiveRush {
28 start_time: f32,
29 start_frame: usize,
30 last_time: f32,
31 last_frame: usize,
32 is_team_0: bool,
33 attackers: usize,
34 defenders: usize,
35 counted: bool,
36}
37
38impl ActiveRush {
39 fn retained_possession_time(&self) -> f32 {
40 (self.last_time - self.start_time).max(0.0)
41 }
42}
43
44#[derive(Debug, Clone, PartialEq)]
45pub struct RushCalculatorConfig {
46 pub max_start_y: f32,
47 pub attack_support_distance_y: f32,
48 pub defender_distance_y: f32,
49 pub min_possession_retained_seconds: f32,
50}
51
52impl Default for RushCalculatorConfig {
53 fn default() -> Self {
54 Self {
55 max_start_y: DEFAULT_RUSH_MAX_START_Y,
56 attack_support_distance_y: DEFAULT_RUSH_ATTACK_SUPPORT_DISTANCE_Y,
57 defender_distance_y: DEFAULT_RUSH_DEFENDER_DISTANCE_Y,
58 min_possession_retained_seconds: DEFAULT_RUSH_MIN_POSSESSION_RETAINED_SECONDS,
59 }
60 }
61}
62
63#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
64pub struct RushStats {
65 pub team_zero_count: u32,
66 pub team_zero_two_v_one_count: u32,
67 pub team_zero_two_v_two_count: u32,
68 pub team_zero_two_v_three_count: u32,
69 pub team_zero_three_v_one_count: u32,
70 pub team_zero_three_v_two_count: u32,
71 pub team_zero_three_v_three_count: u32,
72 pub team_one_count: u32,
73 pub team_one_two_v_one_count: u32,
74 pub team_one_two_v_two_count: u32,
75 pub team_one_two_v_three_count: u32,
76 pub team_one_three_v_one_count: u32,
77 pub team_one_three_v_two_count: u32,
78 pub team_one_three_v_three_count: u32,
79}
80
81impl RushStats {
82 fn record(&mut self, attacking_team_is_team_0: bool, attackers: usize, defenders: usize) {
83 if attacking_team_is_team_0 {
84 self.team_zero_count += 1;
85 match (attackers, defenders) {
86 (2, 1) => self.team_zero_two_v_one_count += 1,
87 (2, 2) => self.team_zero_two_v_two_count += 1,
88 (2, 3) => self.team_zero_two_v_three_count += 1,
89 (3, 1) => self.team_zero_three_v_one_count += 1,
90 (3, 2) => self.team_zero_three_v_two_count += 1,
91 (3, 3) => self.team_zero_three_v_three_count += 1,
92 _ => {}
93 }
94 } else {
95 self.team_one_count += 1;
96 match (attackers, defenders) {
97 (2, 1) => self.team_one_two_v_one_count += 1,
98 (2, 2) => self.team_one_two_v_two_count += 1,
99 (2, 3) => self.team_one_two_v_three_count += 1,
100 (3, 1) => self.team_one_three_v_one_count += 1,
101 (3, 2) => self.team_one_three_v_two_count += 1,
102 (3, 3) => self.team_one_three_v_three_count += 1,
103 _ => {}
104 }
105 }
106 }
107
108 pub fn for_team(&self, is_team_zero: bool) -> RushTeamStats {
109 if is_team_zero {
110 RushTeamStats {
111 count: self.team_zero_count,
112 two_v_one_count: self.team_zero_two_v_one_count,
113 two_v_two_count: self.team_zero_two_v_two_count,
114 two_v_three_count: self.team_zero_two_v_three_count,
115 three_v_one_count: self.team_zero_three_v_one_count,
116 three_v_two_count: self.team_zero_three_v_two_count,
117 three_v_three_count: self.team_zero_three_v_three_count,
118 }
119 } else {
120 RushTeamStats {
121 count: self.team_one_count,
122 two_v_one_count: self.team_one_two_v_one_count,
123 two_v_two_count: self.team_one_two_v_two_count,
124 two_v_three_count: self.team_one_two_v_three_count,
125 three_v_one_count: self.team_one_three_v_one_count,
126 three_v_two_count: self.team_one_three_v_two_count,
127 three_v_three_count: self.team_one_three_v_three_count,
128 }
129 }
130 }
131}
132
133#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, ts_rs::TS)]
134#[ts(export)]
135pub struct RushTeamStats {
136 pub count: u32,
137 pub two_v_one_count: u32,
138 pub two_v_two_count: u32,
139 pub two_v_three_count: u32,
140 pub three_v_one_count: u32,
141 pub three_v_two_count: u32,
142 pub three_v_three_count: u32,
143}
144
145#[derive(Debug, Clone, Default, PartialEq)]
146pub struct RushCalculator {
147 config: RushCalculatorConfig,
148 stats: RushStats,
149 events: Vec<RushEvent>,
150 active_rush: Option<ActiveRush>,
151}
152
153impl RushCalculator {
154 pub fn new() -> Self {
155 Self::with_config(RushCalculatorConfig::default())
156 }
157
158 pub fn with_config(config: RushCalculatorConfig) -> Self {
159 Self {
160 config,
161 ..Self::default()
162 }
163 }
164
165 pub fn config(&self) -> &RushCalculatorConfig {
166 &self.config
167 }
168
169 pub fn stats(&self) -> &RushStats {
170 &self.stats
171 }
172
173 pub fn events(&self) -> &[RushEvent] {
174 &self.events
175 }
176
177 fn record_active_rush(&mut self, active_rush: &mut ActiveRush) {
178 if active_rush.counted {
179 return;
180 }
181 if active_rush.retained_possession_time() < self.config.min_possession_retained_seconds {
182 return;
183 }
184
185 self.stats.record(
186 active_rush.is_team_0,
187 active_rush.attackers,
188 active_rush.defenders,
189 );
190 active_rush.counted = true;
191 }
192
193 fn rush_numbers(
194 &self,
195 ball: &BallFrameState,
196 players: &PlayerFrameState,
197 events: &FrameEventsState,
198 attacking_team_is_team_0: bool,
199 ) -> Option<(usize, usize)> {
200 let ball_position = ball.position()?;
201 let normalized_ball_y = normalized_y(attacking_team_is_team_0, ball_position);
202 if normalized_ball_y > self.config.max_start_y {
203 return None;
204 }
205
206 let demoed_players: HashSet<_> = events
207 .active_demos
208 .iter()
209 .map(|demo| demo.victim.clone())
210 .collect();
211
212 let attackers = players
213 .players
214 .iter()
215 .filter(|player| player.is_team_0 == attacking_team_is_team_0)
216 .filter(|player| !demoed_players.contains(&player.player_id))
217 .filter_map(PlayerSample::position)
218 .filter(|position| {
219 normalized_y(attacking_team_is_team_0, *position)
220 >= normalized_ball_y - self.config.attack_support_distance_y
221 })
222 .count()
223 .min(3);
224
225 let defenders = players
226 .players
227 .iter()
228 .filter(|player| player.is_team_0 != attacking_team_is_team_0)
229 .filter(|player| !demoed_players.contains(&player.player_id))
230 .filter_map(PlayerSample::position)
231 .filter(|position| {
232 normalized_y(attacking_team_is_team_0, *position)
233 >= normalized_ball_y + self.config.defender_distance_y
234 })
235 .count()
236 .min(3);
237
238 if attackers < 2 || defenders == 0 {
239 return None;
240 }
241
242 Some((attackers, defenders))
243 }
244
245 fn finalize_active_rush(&mut self) {
246 let Some(mut active_rush) = self.active_rush.take() else {
247 return;
248 };
249 self.record_active_rush(&mut active_rush);
250 if !active_rush.counted {
251 return;
252 }
253 self.events.push(RushEvent {
254 start_time: active_rush.start_time,
255 start_frame: active_rush.start_frame,
256 end_time: active_rush.last_time,
257 end_frame: active_rush.last_frame,
258 is_team_0: active_rush.is_team_0,
259 attackers: active_rush.attackers,
260 defenders: active_rush.defenders,
261 });
262 }
263
264 fn update_active_rush(
265 &mut self,
266 frame: &FrameInfo,
267 ball: &BallFrameState,
268 players: &PlayerFrameState,
269 events: &FrameEventsState,
270 current_team_is_team_0: Option<bool>,
271 ) {
272 let Some(active_team_is_team_0) = self.active_rush.as_ref().map(|rush| rush.is_team_0)
273 else {
274 return;
275 };
276
277 let active_continues = current_team_is_team_0 == Some(active_team_is_team_0)
278 && self
279 .rush_numbers(ball, players, events, active_team_is_team_0)
280 .is_some();
281 if active_continues {
282 if let Some(active_rush) = self.active_rush.as_mut() {
283 active_rush.last_time = frame.time;
284 active_rush.last_frame = frame.frame_number;
285 }
286 if let Some(mut active_rush) = self.active_rush.take() {
287 self.record_active_rush(&mut active_rush);
288 self.active_rush = Some(active_rush);
289 }
290 return;
291 }
292
293 self.finalize_active_rush();
294 }
295
296 fn maybe_start_rush(
297 &mut self,
298 frame: &FrameInfo,
299 ball: &BallFrameState,
300 players: &PlayerFrameState,
301 events: &FrameEventsState,
302 active_team_before_sample: Option<bool>,
303 current_team_is_team_0: Option<bool>,
304 ) {
305 let Some(attacking_team_is_team_0) = current_team_is_team_0 else {
306 return;
307 };
308 if active_team_before_sample == Some(attacking_team_is_team_0) {
309 return;
310 }
311
312 if let Some((attackers, defenders)) =
313 self.rush_numbers(ball, players, events, attacking_team_is_team_0)
314 {
315 self.active_rush = Some(ActiveRush {
316 start_time: frame.time,
317 start_frame: frame.frame_number,
318 last_time: frame.time,
319 last_frame: frame.frame_number,
320 is_team_0: attacking_team_is_team_0,
321 attackers,
322 defenders,
323 counted: false,
324 });
325 }
326 }
327
328 fn update_rush_state(
329 &mut self,
330 frame: &FrameInfo,
331 ball: &BallFrameState,
332 players: &PlayerFrameState,
333 events: &FrameEventsState,
334 active_team_before_sample: Option<bool>,
335 current_team_is_team_0: Option<bool>,
336 ) {
337 self.update_active_rush(frame, ball, players, events, current_team_is_team_0);
338 if self.active_rush.is_none() {
339 self.maybe_start_rush(
340 frame,
341 ball,
342 players,
343 events,
344 active_team_before_sample,
345 current_team_is_team_0,
346 );
347 }
348 }
349
350 #[allow(clippy::too_many_arguments)]
351 pub fn update_parts(
352 &mut self,
353 frame: &FrameInfo,
354 gameplay: &GameplayState,
355 ball: &BallFrameState,
356 players: &PlayerFrameState,
357 events: &FrameEventsState,
358 possession_state: &PossessionState,
359 live_play_state: &LivePlayState,
360 ) -> SubtrActorResult<()> {
361 if !live_play_state.is_live_play || gameplay.kickoff_phase_active() {
362 self.finalize_active_rush();
363 return Ok(());
364 }
365
366 self.update_rush_state(
367 frame,
368 ball,
369 players,
370 events,
371 possession_state.active_team_before_sample,
372 possession_state.current_team_is_team_0,
373 );
374
375 Ok(())
376 }
377 pub fn finish_calculation(&mut self) -> SubtrActorResult<()> {
378 self.finalize_active_rush();
379 Ok(())
380 }
381}