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 fn dependencies() -> NodeDependencies {
22 vec![
23 frame_info_dependency(),
24 gameplay_state_dependency(),
25 live_play_dependency(),
26 match_stats_dependency(),
27 backboard_dependency(),
28 ceiling_shot_dependency(),
29 wall_aerial_dependency(),
30 wall_aerial_shot_dependency(),
31 double_tap_dependency(),
32 one_timer_dependency(),
33 pass_dependency(),
34 fifty_fifty_dependency(),
35 possession_dependency(),
36 pressure_dependency(),
37 rotation_dependency(),
38 rush_dependency(),
39 touch_dependency(),
40 whiff_dependency(),
41 wavedash_dependency(),
42 speed_flip_dependency(),
43 half_flip_dependency(),
44 flick_dependency(),
45 musty_flick_dependency(),
46 dodge_reset_dependency(),
47 ball_carry_dependency(),
48 boost_dependency(),
49 bump_dependency(),
50 half_volley_dependency(),
51 movement_dependency(),
52 positioning_dependency(),
53 powerslide_dependency(),
54 demo_dependency(),
55 center_dependency(),
56 aerial_goal_dependency(),
57 high_aerial_goal_dependency(),
58 long_distance_goal_dependency(),
59 own_half_goal_dependency(),
60 empty_net_goal_dependency(),
61 counter_attack_goal_dependency(),
62 flick_goal_dependency(),
63 double_tap_goal_dependency(),
64 one_timer_goal_dependency(),
65 passing_goal_dependency(),
66 air_dribble_goal_dependency(),
67 flip_reset_goal_dependency(),
68 half_volley_goal_dependency(),
69 ]
70 }
71
72 fn capture_events(&mut self, ctx: &AnalysisStateContext<'_>) -> SubtrActorResult<()> {
73 let match_stats = ctx.get::<MatchStatsCalculator>()?;
74 let possession = ctx.get::<PossessionCalculator>()?;
75 let pressure = ctx.get::<PressureCalculator>()?;
76 let movement = ctx.get::<MovementCalculator>()?;
77 let positioning = ctx.get::<PositioningCalculator>()?;
78 let rotation = ctx.get::<RotationCalculator>()?;
79 let demo = ctx.get::<DemoCalculator>()?;
80 let backboard = ctx.get::<BackboardCalculator>()?;
81 let ball_carry = ctx.get::<BallCarryCalculator>()?;
82 let ceiling_shot = ctx.get::<CeilingShotCalculator>()?;
83 let wall_aerial = ctx.get::<WallAerialCalculator>()?;
84 let wall_aerial_shot = ctx.get::<WallAerialShotCalculator>()?;
85 let center = ctx.get::<CenterCalculator>()?;
86 let dodge_reset = ctx.get::<DodgeResetCalculator>()?;
87 let double_tap = ctx.get::<DoubleTapCalculator>()?;
88 let one_timer = ctx.get::<OneTimerCalculator>()?;
89 let pass = ctx.get::<PassCalculator>()?;
90 let fifty_fifty = ctx.get::<FiftyFiftyCalculator>()?;
91 let flick = ctx.get::<FlickCalculator>()?;
92 let musty_flick = ctx.get::<MustyFlickCalculator>()?;
93 let aerial_goal = ctx.get::<AerialGoalCalculator>()?;
94 let high_aerial_goal = ctx.get::<HighAerialGoalCalculator>()?;
95 let long_distance_goal = ctx.get::<LongDistanceGoalCalculator>()?;
96 let own_half_goal = ctx.get::<OwnHalfGoalCalculator>()?;
97 let empty_net_goal = ctx.get::<EmptyNetGoalCalculator>()?;
98 let counter_attack_goal = ctx.get::<CounterAttackGoalCalculator>()?;
99 let flick_goal = ctx.get::<FlickGoalCalculator>()?;
100 let double_tap_goal = ctx.get::<DoubleTapGoalCalculator>()?;
101 let one_timer_goal = ctx.get::<OneTimerGoalCalculator>()?;
102 let passing_goal = ctx.get::<PassingGoalCalculator>()?;
103 let air_dribble_goal = ctx.get::<AirDribbleGoalCalculator>()?;
104 let flip_reset_goal = ctx.get::<FlipResetGoalCalculator>()?;
105 let half_volley_goal = ctx.get::<HalfVolleyGoalCalculator>()?;
106 let rush = ctx.get::<RushCalculator>()?;
107 let speed_flip = ctx.get::<SpeedFlipCalculator>()?;
108 let half_flip = ctx.get::<HalfFlipCalculator>()?;
109 let half_volley = ctx.get::<HalfVolleyCalculator>()?;
110 let wavedash = ctx.get::<WavedashCalculator>()?;
111 let whiff = ctx.get::<WhiffCalculator>()?;
112 let powerslide = ctx.get::<PowerslideCalculator>()?;
113 let touch = ctx.get::<TouchCalculator>()?;
114 let boost = ctx.get::<BoostCalculator>()?;
115 let bump = ctx.get::<BumpCalculator>()?;
116
117 let mut timeline = match_stats.timeline().to_vec();
118 timeline.extend(demo.timeline().to_vec());
119 timeline.sort_by(|left, right| left.time.total_cmp(&right.time));
120 let goal_tags = combined_goal_tag_events(&[
121 aerial_goal.events(),
122 high_aerial_goal.events(),
123 long_distance_goal.events(),
124 own_half_goal.events(),
125 empty_net_goal.events(),
126 counter_attack_goal.events(),
127 flick_goal.events(),
128 double_tap_goal.events(),
129 one_timer_goal.events(),
130 passing_goal.events(),
131 air_dribble_goal.events(),
132 flip_reset_goal.events(),
133 half_volley_goal.events(),
134 ]);
135
136 self.state.events = ReplayStatsTimelineEvents {
137 timeline,
138 core_player: match_stats.core_player_events().to_vec(),
139 core_team: match_stats.core_team_events().to_vec(),
140 possession: possession.events().to_vec(),
141 pressure: pressure.events().to_vec(),
142 movement: movement.events().to_vec(),
143 positioning: positioning.events().to_vec(),
144 rotation_player: rotation.player_events().to_vec(),
145 rotation_team: rotation.team_events().to_vec(),
146 mechanics: build_mechanic_events(
147 ball_carry,
148 ceiling_shot,
149 wall_aerial,
150 wall_aerial_shot,
151 center,
152 dodge_reset,
153 double_tap,
154 flick,
155 musty_flick,
156 one_timer,
157 pass,
158 speed_flip,
159 half_flip,
160 half_volley,
161 wavedash,
162 ),
163 goal_context: match_stats.goal_context_events().to_vec(),
164 backboard: backboard.events().to_vec(),
165 ceiling_shot: ceiling_shot.events().to_vec(),
166 wall_aerial: wall_aerial.events().to_vec(),
167 wall_aerial_shot: wall_aerial_shot.events().to_vec(),
168 center: center.events().to_vec(),
169 flick: flick.events().to_vec(),
170 musty_flick: musty_flick.events().to_vec(),
171 dodge_reset: dodge_reset.events().to_vec(),
172 double_tap: double_tap.events().to_vec(),
173 one_timer: one_timer.events().to_vec(),
174 pass: pass.events().to_vec(),
175 pass_last_completed: pass.last_completed_events().to_vec(),
176 ball_carry: ball_carry.carry_events().to_vec(),
177 fifty_fifty: fifty_fifty.events().to_vec(),
178 goal_tags,
179 rush: rush.events().to_vec(),
180 speed_flip: speed_flip.events().to_vec(),
181 half_flip: half_flip.events().to_vec(),
182 half_volley: half_volley.events().to_vec(),
183 wavedash: wavedash.events().to_vec(),
184 whiff: whiff.events().to_vec(),
185 powerslide: powerslide.events().to_vec(),
186 touch: touch.events().to_vec(),
187 touch_ball_movement: touch.ball_movement_events().to_vec(),
188 touch_last_touch: touch.last_touch_events().to_vec(),
189 boost_pickups: boost.pickup_comparison_events().to_vec(),
190 boost_ledger: boost.ledger_events().to_vec(),
191 boost_state: boost.state_events().to_vec(),
192 bump: bump.events().to_vec(),
193 };
194 Ok(())
195 }
196}
197
198impl Default for StatsTimelineEventsNode {
199 fn default() -> Self {
200 Self::new()
201 }
202}
203
204impl AnalysisNode for StatsTimelineEventsNode {
205 type State = StatsTimelineEventsState;
206
207 fn name(&self) -> &'static str {
208 "stats_timeline_events"
209 }
210
211 fn dependencies(&self) -> Vec<AnalysisDependency> {
212 Self::dependencies()
213 }
214
215 fn evaluate(&mut self, _ctx: &AnalysisStateContext<'_>) -> SubtrActorResult<()> {
216 Ok(())
217 }
218
219 fn finish(&mut self, ctx: &AnalysisStateContext<'_>) -> SubtrActorResult<()> {
220 self.capture_events(ctx)
221 }
222
223 fn state(&self) -> &Self::State {
224 &self.state
225 }
226}
227
228pub(crate) fn boxed_default() -> Box<dyn AnalysisNodeDyn> {
229 Box::new(StatsTimelineEventsNode::new())
230}
231
232fn moment_mechanic_event(
233 kind: &str,
234 index: usize,
235 frame: usize,
236 time: f32,
237 player_id: PlayerId,
238 is_team_0: bool,
239) -> MechanicEvent {
240 MechanicEvent {
241 id: format!("{kind}:{frame}:{index}"),
242 kind: kind.to_owned(),
243 player_id,
244 is_team_0,
245 timing: MechanicTiming::Moment { frame, time },
246 properties: Vec::new(),
247 }
248}
249
250#[allow(clippy::too_many_arguments)]
251fn span_mechanic_event(
252 kind: &str,
253 index: usize,
254 start_frame: usize,
255 end_frame: usize,
256 start_time: f32,
257 end_time: f32,
258 player_id: PlayerId,
259 is_team_0: bool,
260) -> MechanicEvent {
261 MechanicEvent {
262 id: format!("{kind}:{start_frame}:{end_frame}:{index}"),
263 kind: kind.to_owned(),
264 player_id,
265 is_team_0,
266 timing: MechanicTiming::Span {
267 start_frame,
268 end_frame,
269 start_time,
270 end_time,
271 },
272 properties: Vec::new(),
273 }
274}
275
276fn mechanic_event_text_property(key: &str, value: &str) -> MechanicEventProperty {
277 MechanicEventProperty {
278 key: key.to_owned(),
279 value: MechanicEventPropertyValue::Text(value.to_owned()),
280 }
281}
282
283fn mechanic_event_unsigned_property(key: &str, value: u32) -> MechanicEventProperty {
284 MechanicEventProperty {
285 key: key.to_owned(),
286 value: MechanicEventPropertyValue::Unsigned(value),
287 }
288}
289
290fn ball_carry_mechanic_event_properties(event: &BallCarryEvent) -> Vec<MechanicEventProperty> {
291 let mut properties = Vec::new();
292 if let Some(origin) = event.air_dribble_origin {
293 properties.push(mechanic_event_text_property(
294 "origin",
295 origin.as_label_value(),
296 ));
297 }
298 if event.kind == BallCarryKind::AirDribble {
299 properties.push(mechanic_event_unsigned_property(
300 "touch_count",
301 event.touch_count,
302 ));
303 }
304 properties
305}
306
307#[allow(clippy::too_many_arguments)]
308fn build_mechanic_events(
309 ball_carry: &BallCarryCalculator,
310 ceiling_shot: &CeilingShotCalculator,
311 wall_aerial: &WallAerialCalculator,
312 wall_aerial_shot: &WallAerialShotCalculator,
313 center: &CenterCalculator,
314 dodge_reset: &DodgeResetCalculator,
315 double_tap: &DoubleTapCalculator,
316 flick: &FlickCalculator,
317 musty_flick: &MustyFlickCalculator,
318 one_timer: &OneTimerCalculator,
319 pass: &PassCalculator,
320 speed_flip: &SpeedFlipCalculator,
321 half_flip: &HalfFlipCalculator,
322 half_volley: &HalfVolleyCalculator,
323 wavedash: &WavedashCalculator,
324) -> Vec<MechanicEvent> {
325 let mut events = Vec::new();
326
327 for (index, event) in ball_carry.carry_events().iter().enumerate() {
328 let kind = match event.kind {
329 BallCarryKind::Carry => "ball_carry",
330 BallCarryKind::AirDribble => "air_dribble",
331 };
332 let mut mechanic_event = span_mechanic_event(
333 kind,
334 index,
335 event.start_frame,
336 event.end_frame,
337 event.start_time,
338 event.end_time,
339 event.player_id.clone(),
340 event.is_team_0,
341 );
342 mechanic_event.properties = ball_carry_mechanic_event_properties(event);
343 events.push(mechanic_event);
344 }
345
346 for (index, event) in ceiling_shot.events().iter().enumerate() {
347 events.push(span_mechanic_event(
348 "ceiling_shot",
349 index,
350 event.ceiling_contact_frame,
351 event.frame,
352 event.ceiling_contact_time,
353 event.time,
354 event.player.clone(),
355 event.is_team_0,
356 ));
357 }
358
359 for (index, event) in wall_aerial.events().iter().enumerate() {
360 let mut mechanic_event = span_mechanic_event(
361 "wall_aerial",
362 index,
363 event.wall_contact_frame,
364 event.frame,
365 event.wall_contact_time,
366 event.time,
367 event.player.clone(),
368 event.is_team_0,
369 );
370 mechanic_event.properties = vec![mechanic_event_text_property(
371 "wall",
372 event.wall.as_label_value(),
373 )];
374 events.push(mechanic_event);
375 }
376
377 for (index, event) in wall_aerial_shot.events().iter().enumerate() {
378 let mut mechanic_event = span_mechanic_event(
379 "wall_aerial_shot",
380 index,
381 event.takeoff_frame,
382 event.frame,
383 event.takeoff_time,
384 event.time,
385 event.player.clone(),
386 event.is_team_0,
387 );
388 mechanic_event.properties = vec![mechanic_event_text_property(
389 "wall",
390 event.wall.as_label_value(),
391 )];
392 events.push(mechanic_event);
393 }
394
395 for (index, event) in center.events().iter().enumerate() {
396 events.push(span_mechanic_event(
397 "center",
398 index,
399 event.start_frame,
400 event.frame,
401 event.start_time,
402 event.time,
403 event.player.clone(),
404 event.is_team_0,
405 ));
406 }
407
408 for (index, event) in dodge_reset.on_ball_events().iter().enumerate() {
409 events.push(moment_mechanic_event(
410 "flip_reset",
411 index,
412 event.frame,
413 event.time,
414 event.player.clone(),
415 event.is_team_0,
416 ));
417 }
418
419 for (index, event) in double_tap.events().iter().enumerate() {
420 events.push(span_mechanic_event(
421 "double_tap",
422 index,
423 event.backboard_frame,
424 event.frame,
425 event.backboard_time,
426 event.time,
427 event.player.clone(),
428 event.is_team_0,
429 ));
430 }
431
432 for (index, event) in flick.events().iter().enumerate() {
433 events.push(span_mechanic_event(
434 "flick",
435 index,
436 event.setup_start_frame,
437 event.frame,
438 event.setup_start_time,
439 event.time,
440 event.player.clone(),
441 event.is_team_0,
442 ));
443 }
444
445 for (index, event) in musty_flick.events().iter().enumerate() {
446 events.push(span_mechanic_event(
447 "musty_flick",
448 index,
449 event.dodge_frame,
450 event.frame,
451 event.dodge_time,
452 event.time,
453 event.player.clone(),
454 event.is_team_0,
455 ));
456 }
457
458 for (index, event) in one_timer.events().iter().enumerate() {
459 events.push(span_mechanic_event(
460 "one_timer",
461 index,
462 event.pass_start_frame,
463 event.frame,
464 event.pass_start_time,
465 event.time,
466 event.player.clone(),
467 event.is_team_0,
468 ));
469 }
470
471 for (index, event) in pass.events().iter().enumerate() {
472 events.push(span_mechanic_event(
473 "pass",
474 index,
475 event.start_frame,
476 event.frame,
477 event.start_time,
478 event.time,
479 event.passer.clone(),
480 event.is_team_0,
481 ));
482 }
483
484 for (index, event) in speed_flip.events().iter().enumerate() {
485 events.push(moment_mechanic_event(
486 "speed_flip",
487 index,
488 event.frame,
489 event.time,
490 event.player.clone(),
491 event.is_team_0,
492 ));
493 }
494
495 for (index, event) in half_flip.events().iter().enumerate() {
496 events.push(moment_mechanic_event(
497 "half_flip",
498 index,
499 event.frame,
500 event.time,
501 event.player.clone(),
502 event.is_team_0,
503 ));
504 }
505
506 for (index, event) in half_volley.events().iter().enumerate() {
507 events.push(moment_mechanic_event(
508 "half_volley",
509 index,
510 event.frame,
511 event.time,
512 event.player.clone(),
513 event.is_team_0,
514 ));
515 }
516
517 for (index, event) in wavedash.events().iter().enumerate() {
518 events.push(span_mechanic_event(
519 "wavedash",
520 index,
521 event.dodge_frame,
522 event.frame,
523 event.dodge_time,
524 event.time,
525 event.player.clone(),
526 event.is_team_0,
527 ));
528 }
529
530 events.sort_by(|left, right| {
531 let left_time = mechanic_event_start_time(left);
532 let right_time = mechanic_event_start_time(right);
533 left_time
534 .total_cmp(&right_time)
535 .then_with(|| left.kind.cmp(&right.kind))
536 .then_with(|| left.id.cmp(&right.id))
537 });
538 events
539}
540
541fn mechanic_event_start_time(event: &MechanicEvent) -> f32 {
542 match event.timing {
543 MechanicTiming::Moment { time, .. } => time,
544 MechanicTiming::Span { start_time, .. } => start_time,
545 }
546}