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