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