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}