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