1use super::*;
2use crate::stats::calculators::*;
3use crate::*;
4
5#[derive(Debug, Clone, Default)]
6pub struct StatsTimelineEventsState {
7 pub events: ReplayStatsTimelineEvents,
8}
9
10pub struct StatsTimelineEventsNode {
11 state: StatsTimelineEventsState,
12}
13
14impl StatsTimelineEventsNode {
15 pub fn new() -> Self {
16 Self {
17 state: StatsTimelineEventsState::default(),
18 }
19 }
20}
21
22impl_analysis_node! {
23 node = StatsTimelineEventsNode,
24 state = StatsTimelineEventsState,
25 name = "stats_timeline_events",
26 dependencies = [
27 match_stats_dependency(),
28 demo_dependency(),
29 backboard_dependency(),
30 ball_carry_dependency(),
31 ceiling_shot_dependency(),
32 center_dependency(),
33 dodge_reset_dependency(),
34 double_tap_dependency(),
35 one_timer_dependency(),
36 pass_dependency(),
37 fifty_fifty_dependency(),
38 flick_dependency(),
39 musty_flick_dependency(),
40 aerial_goal_dependency(),
41 high_aerial_goal_dependency(),
42 long_distance_goal_dependency(),
43 own_half_goal_dependency(),
44 empty_net_goal_dependency(),
45 flick_goal_dependency(),
46 one_timer_goal_dependency(),
47 air_dribble_goal_dependency(),
48 flip_reset_goal_dependency(),
49 half_volley_goal_dependency(),
50 rush_dependency(),
51 speed_flip_dependency(),
52 half_flip_dependency(),
53 half_volley_dependency(),
54 wavedash_dependency(),
55 whiff_dependency(),
56 boost_dependency(),
57 bump_dependency(),
58 ],
59 inputs = {
60 match_stats: MatchStatsCalculator,
61 demo: DemoCalculator,
62 backboard: BackboardCalculator,
63 ball_carry: BallCarryCalculator,
64 ceiling_shot: CeilingShotCalculator,
65 center: CenterCalculator,
66 dodge_reset: DodgeResetCalculator,
67 double_tap: DoubleTapCalculator,
68 one_timer: OneTimerCalculator,
69 pass: PassCalculator,
70 fifty_fifty: FiftyFiftyCalculator,
71 flick: FlickCalculator,
72 musty_flick: MustyFlickCalculator,
73 aerial_goal: AerialGoalCalculator,
74 high_aerial_goal: HighAerialGoalCalculator,
75 long_distance_goal: LongDistanceGoalCalculator,
76 own_half_goal: OwnHalfGoalCalculator,
77 empty_net_goal: EmptyNetGoalCalculator,
78 flick_goal: FlickGoalCalculator,
79 one_timer_goal: OneTimerGoalCalculator,
80 air_dribble_goal: AirDribbleGoalCalculator,
81 flip_reset_goal: FlipResetGoalCalculator,
82 half_volley_goal: HalfVolleyGoalCalculator,
83 rush: RushCalculator,
84 speed_flip: SpeedFlipCalculator,
85 half_flip: HalfFlipCalculator,
86 half_volley: HalfVolleyCalculator,
87 wavedash: WavedashCalculator,
88 whiff: WhiffCalculator,
89 boost: BoostCalculator,
90 bump: BumpCalculator,
91 },
92 evaluate = |node| {
93 let mut timeline = match_stats.timeline().to_vec();
94 timeline.extend(demo.timeline().to_vec());
95 timeline.sort_by(|left, right| left.time.total_cmp(&right.time));
96 let goal_tags = combined_goal_tag_events(&[
97 aerial_goal.events(),
98 high_aerial_goal.events(),
99 long_distance_goal.events(),
100 own_half_goal.events(),
101 empty_net_goal.events(),
102 flick_goal.events(),
103 one_timer_goal.events(),
104 air_dribble_goal.events(),
105 flip_reset_goal.events(),
106 half_volley_goal.events(),
107 ]);
108
109 node.state.events = ReplayStatsTimelineEvents {
110 timeline,
111 mechanics: build_mechanic_events(
112 ball_carry,
113 ceiling_shot,
114 center,
115 dodge_reset,
116 double_tap,
117 flick,
118 musty_flick,
119 one_timer,
120 pass,
121 speed_flip,
122 half_flip,
123 half_volley,
124 wavedash,
125 ),
126 goal_context: match_stats.goal_context_events().to_vec(),
127 backboard: backboard.events().to_vec(),
128 ceiling_shot: ceiling_shot.events().to_vec(),
129 center: center.events().to_vec(),
130 double_tap: double_tap.events().to_vec(),
131 one_timer: one_timer.events().to_vec(),
132 pass: pass.events().to_vec(),
133 fifty_fifty: fifty_fifty.events().to_vec(),
134 goal_tags,
135 rush: rush.events().to_vec(),
136 speed_flip: speed_flip.events().to_vec(),
137 half_flip: half_flip.events().to_vec(),
138 half_volley: half_volley.events().to_vec(),
139 wavedash: wavedash.events().to_vec(),
140 whiff: whiff.events().to_vec(),
141 boost_pickups: boost.pickup_comparison_events().to_vec(),
142 bump: bump.events().to_vec(),
143 };
144 Ok(())
145 },
146 state_ref = |node| &node.state,
147}
148
149fn moment_mechanic_event(
150 kind: &str,
151 index: usize,
152 frame: usize,
153 time: f32,
154 player_id: PlayerId,
155 is_team_0: bool,
156) -> MechanicEvent {
157 MechanicEvent {
158 id: format!("{kind}:{frame}:{index}"),
159 kind: kind.to_owned(),
160 player_id,
161 is_team_0,
162 timing: MechanicTiming::Moment { frame, time },
163 properties: Vec::new(),
164 }
165}
166
167#[allow(clippy::too_many_arguments)]
168fn span_mechanic_event(
169 kind: &str,
170 index: usize,
171 start_frame: usize,
172 end_frame: usize,
173 start_time: f32,
174 end_time: f32,
175 player_id: PlayerId,
176 is_team_0: bool,
177) -> MechanicEvent {
178 MechanicEvent {
179 id: format!("{kind}:{start_frame}:{end_frame}:{index}"),
180 kind: kind.to_owned(),
181 player_id,
182 is_team_0,
183 timing: MechanicTiming::Span {
184 start_frame,
185 end_frame,
186 start_time,
187 end_time,
188 },
189 properties: Vec::new(),
190 }
191}
192
193fn mechanic_event_text_property(key: &str, value: &str) -> MechanicEventProperty {
194 MechanicEventProperty {
195 key: key.to_owned(),
196 value: MechanicEventPropertyValue::Text(value.to_owned()),
197 }
198}
199
200fn mechanic_event_unsigned_property(key: &str, value: u32) -> MechanicEventProperty {
201 MechanicEventProperty {
202 key: key.to_owned(),
203 value: MechanicEventPropertyValue::Unsigned(value),
204 }
205}
206
207fn ball_carry_mechanic_event_properties(event: &BallCarryEvent) -> Vec<MechanicEventProperty> {
208 let mut properties = Vec::new();
209 if let Some(origin) = event.air_dribble_origin {
210 properties.push(mechanic_event_text_property(
211 "origin",
212 origin.as_label_value(),
213 ));
214 }
215 if event.kind == BallCarryKind::AirDribble {
216 properties.push(mechanic_event_unsigned_property(
217 "touch_count",
218 event.touch_count,
219 ));
220 }
221 properties
222}
223
224#[allow(clippy::too_many_arguments)]
225fn build_mechanic_events(
226 ball_carry: &BallCarryCalculator,
227 ceiling_shot: &CeilingShotCalculator,
228 center: &CenterCalculator,
229 dodge_reset: &DodgeResetCalculator,
230 double_tap: &DoubleTapCalculator,
231 flick: &FlickCalculator,
232 musty_flick: &MustyFlickCalculator,
233 one_timer: &OneTimerCalculator,
234 pass: &PassCalculator,
235 speed_flip: &SpeedFlipCalculator,
236 half_flip: &HalfFlipCalculator,
237 half_volley: &HalfVolleyCalculator,
238 wavedash: &WavedashCalculator,
239) -> Vec<MechanicEvent> {
240 let mut events = Vec::new();
241
242 for (index, event) in ball_carry.carry_events().iter().enumerate() {
243 let kind = match event.kind {
244 BallCarryKind::Carry => "ball_carry",
245 BallCarryKind::AirDribble => "air_dribble",
246 };
247 let mut mechanic_event = span_mechanic_event(
248 kind,
249 index,
250 event.start_frame,
251 event.end_frame,
252 event.start_time,
253 event.end_time,
254 event.player_id.clone(),
255 event.is_team_0,
256 );
257 mechanic_event.properties = ball_carry_mechanic_event_properties(event);
258 events.push(mechanic_event);
259 }
260
261 for (index, event) in ceiling_shot.events().iter().enumerate() {
262 events.push(span_mechanic_event(
263 "ceiling_shot",
264 index,
265 event.ceiling_contact_frame,
266 event.frame,
267 event.ceiling_contact_time,
268 event.time,
269 event.player.clone(),
270 event.is_team_0,
271 ));
272 }
273
274 for (index, event) in center.events().iter().enumerate() {
275 events.push(span_mechanic_event(
276 "center",
277 index,
278 event.start_frame,
279 event.frame,
280 event.start_time,
281 event.time,
282 event.player.clone(),
283 event.is_team_0,
284 ));
285 }
286
287 for (index, event) in dodge_reset.on_ball_events().iter().enumerate() {
288 events.push(moment_mechanic_event(
289 "flip_reset",
290 index,
291 event.frame,
292 event.time,
293 event.player.clone(),
294 event.is_team_0,
295 ));
296 }
297
298 for (index, event) in double_tap.events().iter().enumerate() {
299 events.push(span_mechanic_event(
300 "double_tap",
301 index,
302 event.backboard_frame,
303 event.frame,
304 event.backboard_time,
305 event.time,
306 event.player.clone(),
307 event.is_team_0,
308 ));
309 }
310
311 for (index, event) in flick.events().iter().enumerate() {
312 events.push(span_mechanic_event(
313 "flick",
314 index,
315 event.setup_start_frame,
316 event.frame,
317 event.setup_start_time,
318 event.time,
319 event.player.clone(),
320 event.is_team_0,
321 ));
322 }
323
324 for (index, event) in musty_flick.events().iter().enumerate() {
325 events.push(span_mechanic_event(
326 "musty_flick",
327 index,
328 event.dodge_frame,
329 event.frame,
330 event.dodge_time,
331 event.time,
332 event.player.clone(),
333 event.is_team_0,
334 ));
335 }
336
337 for (index, event) in one_timer.events().iter().enumerate() {
338 events.push(span_mechanic_event(
339 "one_timer",
340 index,
341 event.pass_start_frame,
342 event.frame,
343 event.pass_start_time,
344 event.time,
345 event.player.clone(),
346 event.is_team_0,
347 ));
348 }
349
350 for (index, event) in pass.events().iter().enumerate() {
351 events.push(span_mechanic_event(
352 "pass",
353 index,
354 event.start_frame,
355 event.frame,
356 event.start_time,
357 event.time,
358 event.passer.clone(),
359 event.is_team_0,
360 ));
361 }
362
363 for (index, event) in speed_flip.events().iter().enumerate() {
364 events.push(moment_mechanic_event(
365 "speed_flip",
366 index,
367 event.frame,
368 event.time,
369 event.player.clone(),
370 event.is_team_0,
371 ));
372 }
373
374 for (index, event) in half_flip.events().iter().enumerate() {
375 events.push(moment_mechanic_event(
376 "half_flip",
377 index,
378 event.frame,
379 event.time,
380 event.player.clone(),
381 event.is_team_0,
382 ));
383 }
384
385 for (index, event) in half_volley.events().iter().enumerate() {
386 events.push(moment_mechanic_event(
387 "half_volley",
388 index,
389 event.frame,
390 event.time,
391 event.player.clone(),
392 event.is_team_0,
393 ));
394 }
395
396 for (index, event) in wavedash.events().iter().enumerate() {
397 events.push(span_mechanic_event(
398 "wavedash",
399 index,
400 event.dodge_frame,
401 event.frame,
402 event.dodge_time,
403 event.time,
404 event.player.clone(),
405 event.is_team_0,
406 ));
407 }
408
409 events.sort_by(|left, right| {
410 let left_time = mechanic_event_start_time(left);
411 let right_time = mechanic_event_start_time(right);
412 left_time
413 .total_cmp(&right_time)
414 .then_with(|| left.kind.cmp(&right.kind))
415 .then_with(|| left.id.cmp(&right.id))
416 });
417 events
418}
419
420fn mechanic_event_start_time(event: &MechanicEvent) -> f32 {
421 match event.timing {
422 MechanicTiming::Moment { time, .. } => time,
423 MechanicTiming::Span { start_time, .. } => start_time,
424 }
425}