Skip to main content

subtr_actor/stats/analysis_graph/nodes/
stats_timeline_events.rs

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}