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