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