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            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}