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