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
22impl_analysis_node! {
23    node = StatsTimelineEventsNode,
24    state = StatsTimelineEventsState,
25    name = "stats_timeline_events",
26    dependencies = [
27        match_stats_dependency(),
28        demo_dependency(),
29        backboard_dependency(),
30        ball_carry_dependency(),
31        ceiling_shot_dependency(),
32        center_dependency(),
33        dodge_reset_dependency(),
34        double_tap_dependency(),
35        one_timer_dependency(),
36        pass_dependency(),
37        fifty_fifty_dependency(),
38        flick_dependency(),
39        musty_flick_dependency(),
40        aerial_goal_dependency(),
41        high_aerial_goal_dependency(),
42        long_distance_goal_dependency(),
43        own_half_goal_dependency(),
44        empty_net_goal_dependency(),
45        flick_goal_dependency(),
46        one_timer_goal_dependency(),
47        air_dribble_goal_dependency(),
48        flip_reset_goal_dependency(),
49        half_volley_goal_dependency(),
50        rush_dependency(),
51        speed_flip_dependency(),
52        half_flip_dependency(),
53        half_volley_dependency(),
54        wavedash_dependency(),
55        whiff_dependency(),
56        boost_dependency(),
57        bump_dependency(),
58    ],
59    inputs = {
60        match_stats: MatchStatsCalculator,
61        demo: DemoCalculator,
62        backboard: BackboardCalculator,
63        ball_carry: BallCarryCalculator,
64        ceiling_shot: CeilingShotCalculator,
65        center: CenterCalculator,
66        dodge_reset: DodgeResetCalculator,
67        double_tap: DoubleTapCalculator,
68        one_timer: OneTimerCalculator,
69        pass: PassCalculator,
70        fifty_fifty: FiftyFiftyCalculator,
71        flick: FlickCalculator,
72        musty_flick: MustyFlickCalculator,
73        aerial_goal: AerialGoalCalculator,
74        high_aerial_goal: HighAerialGoalCalculator,
75        long_distance_goal: LongDistanceGoalCalculator,
76        own_half_goal: OwnHalfGoalCalculator,
77        empty_net_goal: EmptyNetGoalCalculator,
78        flick_goal: FlickGoalCalculator,
79        one_timer_goal: OneTimerGoalCalculator,
80        air_dribble_goal: AirDribbleGoalCalculator,
81        flip_reset_goal: FlipResetGoalCalculator,
82        half_volley_goal: HalfVolleyGoalCalculator,
83        rush: RushCalculator,
84        speed_flip: SpeedFlipCalculator,
85        half_flip: HalfFlipCalculator,
86        half_volley: HalfVolleyCalculator,
87        wavedash: WavedashCalculator,
88        whiff: WhiffCalculator,
89        boost: BoostCalculator,
90        bump: BumpCalculator,
91    },
92    evaluate = |node| {
93        let mut timeline = match_stats.timeline().to_vec();
94        timeline.extend(demo.timeline().to_vec());
95        timeline.sort_by(|left, right| left.time.total_cmp(&right.time));
96        let goal_tags = combined_goal_tag_events(&[
97            aerial_goal.events(),
98            high_aerial_goal.events(),
99            long_distance_goal.events(),
100            own_half_goal.events(),
101            empty_net_goal.events(),
102            flick_goal.events(),
103            one_timer_goal.events(),
104            air_dribble_goal.events(),
105            flip_reset_goal.events(),
106            half_volley_goal.events(),
107        ]);
108
109        node.state.events = ReplayStatsTimelineEvents {
110            timeline,
111            mechanics: build_mechanic_events(
112                ball_carry,
113                ceiling_shot,
114                center,
115                dodge_reset,
116                double_tap,
117                flick,
118                musty_flick,
119                one_timer,
120                pass,
121                speed_flip,
122                half_flip,
123                half_volley,
124                wavedash,
125            ),
126            goal_context: match_stats.goal_context_events().to_vec(),
127            backboard: backboard.events().to_vec(),
128            ceiling_shot: ceiling_shot.events().to_vec(),
129            center: center.events().to_vec(),
130            double_tap: double_tap.events().to_vec(),
131            one_timer: one_timer.events().to_vec(),
132            pass: pass.events().to_vec(),
133            fifty_fifty: fifty_fifty.events().to_vec(),
134            goal_tags,
135            rush: rush.events().to_vec(),
136            speed_flip: speed_flip.events().to_vec(),
137            half_flip: half_flip.events().to_vec(),
138            half_volley: half_volley.events().to_vec(),
139            wavedash: wavedash.events().to_vec(),
140            whiff: whiff.events().to_vec(),
141            boost_pickups: boost.pickup_comparison_events().to_vec(),
142            bump: bump.events().to_vec(),
143        };
144        Ok(())
145    },
146    state_ref = |node| &node.state,
147}
148
149fn moment_mechanic_event(
150    kind: &str,
151    index: usize,
152    frame: usize,
153    time: f32,
154    player_id: PlayerId,
155    is_team_0: bool,
156) -> MechanicEvent {
157    MechanicEvent {
158        id: format!("{kind}:{frame}:{index}"),
159        kind: kind.to_owned(),
160        player_id,
161        is_team_0,
162        timing: MechanicTiming::Moment { frame, time },
163        properties: Vec::new(),
164    }
165}
166
167#[allow(clippy::too_many_arguments)]
168fn span_mechanic_event(
169    kind: &str,
170    index: usize,
171    start_frame: usize,
172    end_frame: usize,
173    start_time: f32,
174    end_time: f32,
175    player_id: PlayerId,
176    is_team_0: bool,
177) -> MechanicEvent {
178    MechanicEvent {
179        id: format!("{kind}:{start_frame}:{end_frame}:{index}"),
180        kind: kind.to_owned(),
181        player_id,
182        is_team_0,
183        timing: MechanicTiming::Span {
184            start_frame,
185            end_frame,
186            start_time,
187            end_time,
188        },
189        properties: Vec::new(),
190    }
191}
192
193fn mechanic_event_text_property(key: &str, value: &str) -> MechanicEventProperty {
194    MechanicEventProperty {
195        key: key.to_owned(),
196        value: MechanicEventPropertyValue::Text(value.to_owned()),
197    }
198}
199
200fn mechanic_event_unsigned_property(key: &str, value: u32) -> MechanicEventProperty {
201    MechanicEventProperty {
202        key: key.to_owned(),
203        value: MechanicEventPropertyValue::Unsigned(value),
204    }
205}
206
207fn ball_carry_mechanic_event_properties(event: &BallCarryEvent) -> Vec<MechanicEventProperty> {
208    let mut properties = Vec::new();
209    if let Some(origin) = event.air_dribble_origin {
210        properties.push(mechanic_event_text_property(
211            "origin",
212            origin.as_label_value(),
213        ));
214    }
215    if event.kind == BallCarryKind::AirDribble {
216        properties.push(mechanic_event_unsigned_property(
217            "touch_count",
218            event.touch_count,
219        ));
220    }
221    properties
222}
223
224#[allow(clippy::too_many_arguments)]
225fn build_mechanic_events(
226    ball_carry: &BallCarryCalculator,
227    ceiling_shot: &CeilingShotCalculator,
228    center: &CenterCalculator,
229    dodge_reset: &DodgeResetCalculator,
230    double_tap: &DoubleTapCalculator,
231    flick: &FlickCalculator,
232    musty_flick: &MustyFlickCalculator,
233    one_timer: &OneTimerCalculator,
234    pass: &PassCalculator,
235    speed_flip: &SpeedFlipCalculator,
236    half_flip: &HalfFlipCalculator,
237    half_volley: &HalfVolleyCalculator,
238    wavedash: &WavedashCalculator,
239) -> Vec<MechanicEvent> {
240    let mut events = Vec::new();
241
242    for (index, event) in ball_carry.carry_events().iter().enumerate() {
243        let kind = match event.kind {
244            BallCarryKind::Carry => "ball_carry",
245            BallCarryKind::AirDribble => "air_dribble",
246        };
247        let mut mechanic_event = span_mechanic_event(
248            kind,
249            index,
250            event.start_frame,
251            event.end_frame,
252            event.start_time,
253            event.end_time,
254            event.player_id.clone(),
255            event.is_team_0,
256        );
257        mechanic_event.properties = ball_carry_mechanic_event_properties(event);
258        events.push(mechanic_event);
259    }
260
261    for (index, event) in ceiling_shot.events().iter().enumerate() {
262        events.push(span_mechanic_event(
263            "ceiling_shot",
264            index,
265            event.ceiling_contact_frame,
266            event.frame,
267            event.ceiling_contact_time,
268            event.time,
269            event.player.clone(),
270            event.is_team_0,
271        ));
272    }
273
274    for (index, event) in center.events().iter().enumerate() {
275        events.push(span_mechanic_event(
276            "center",
277            index,
278            event.start_frame,
279            event.frame,
280            event.start_time,
281            event.time,
282            event.player.clone(),
283            event.is_team_0,
284        ));
285    }
286
287    for (index, event) in dodge_reset.on_ball_events().iter().enumerate() {
288        events.push(moment_mechanic_event(
289            "flip_reset",
290            index,
291            event.frame,
292            event.time,
293            event.player.clone(),
294            event.is_team_0,
295        ));
296    }
297
298    for (index, event) in double_tap.events().iter().enumerate() {
299        events.push(span_mechanic_event(
300            "double_tap",
301            index,
302            event.backboard_frame,
303            event.frame,
304            event.backboard_time,
305            event.time,
306            event.player.clone(),
307            event.is_team_0,
308        ));
309    }
310
311    for (index, event) in flick.events().iter().enumerate() {
312        events.push(span_mechanic_event(
313            "flick",
314            index,
315            event.setup_start_frame,
316            event.frame,
317            event.setup_start_time,
318            event.time,
319            event.player.clone(),
320            event.is_team_0,
321        ));
322    }
323
324    for (index, event) in musty_flick.events().iter().enumerate() {
325        events.push(span_mechanic_event(
326            "musty_flick",
327            index,
328            event.dodge_frame,
329            event.frame,
330            event.dodge_time,
331            event.time,
332            event.player.clone(),
333            event.is_team_0,
334        ));
335    }
336
337    for (index, event) in one_timer.events().iter().enumerate() {
338        events.push(span_mechanic_event(
339            "one_timer",
340            index,
341            event.pass_start_frame,
342            event.frame,
343            event.pass_start_time,
344            event.time,
345            event.player.clone(),
346            event.is_team_0,
347        ));
348    }
349
350    for (index, event) in pass.events().iter().enumerate() {
351        events.push(span_mechanic_event(
352            "pass",
353            index,
354            event.start_frame,
355            event.frame,
356            event.start_time,
357            event.time,
358            event.passer.clone(),
359            event.is_team_0,
360        ));
361    }
362
363    for (index, event) in speed_flip.events().iter().enumerate() {
364        events.push(moment_mechanic_event(
365            "speed_flip",
366            index,
367            event.frame,
368            event.time,
369            event.player.clone(),
370            event.is_team_0,
371        ));
372    }
373
374    for (index, event) in half_flip.events().iter().enumerate() {
375        events.push(moment_mechanic_event(
376            "half_flip",
377            index,
378            event.frame,
379            event.time,
380            event.player.clone(),
381            event.is_team_0,
382        ));
383    }
384
385    for (index, event) in half_volley.events().iter().enumerate() {
386        events.push(moment_mechanic_event(
387            "half_volley",
388            index,
389            event.frame,
390            event.time,
391            event.player.clone(),
392            event.is_team_0,
393        ));
394    }
395
396    for (index, event) in wavedash.events().iter().enumerate() {
397        events.push(span_mechanic_event(
398            "wavedash",
399            index,
400            event.dodge_frame,
401            event.frame,
402            event.dodge_time,
403            event.time,
404            event.player.clone(),
405            event.is_team_0,
406        ));
407    }
408
409    events.sort_by(|left, right| {
410        let left_time = mechanic_event_start_time(left);
411        let right_time = mechanic_event_start_time(right);
412        left_time
413            .total_cmp(&right_time)
414            .then_with(|| left.kind.cmp(&right.kind))
415            .then_with(|| left.id.cmp(&right.id))
416    });
417    events
418}
419
420fn mechanic_event_start_time(event: &MechanicEvent) -> f32 {
421    match event.timing {
422        MechanicTiming::Moment { time, .. } => time,
423        MechanicTiming::Span { start_time, .. } => start_time,
424    }
425}