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 ),
115 goal_context: match_stats.goal_context_events().to_vec(),
116 backboard: backboard.events().to_vec(),
117 ceiling_shot: ceiling_shot.events().to_vec(),
118 double_tap: double_tap.events().to_vec(),
119 one_timer: one_timer.events().to_vec(),
120 pass: pass.events().to_vec(),
121 fifty_fifty: fifty_fifty.events().to_vec(),
122 goal_tags,
123 rush: rush.events().to_vec(),
124 speed_flip: speed_flip.events().to_vec(),
125 half_flip: half_flip.events().to_vec(),
126 wavedash: wavedash.events().to_vec(),
127 whiff: whiff.events().to_vec(),
128 boost_pickups: boost.pickup_comparison_events().to_vec(),
129 };
130 Ok(())
131 },
132 state_ref = |node| &node.state,
133}
134
135fn moment_mechanic_event(
136 kind: &str,
137 index: usize,
138 frame: usize,
139 time: f32,
140 player_id: PlayerId,
141 is_team_0: bool,
142) -> MechanicEvent {
143 MechanicEvent {
144 id: format!("{kind}:{frame}:{index}"),
145 kind: kind.to_owned(),
146 player_id,
147 is_team_0,
148 timing: MechanicTiming::Moment { frame, time },
149 }
150}
151
152#[allow(clippy::too_many_arguments)]
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) -> Vec<MechanicEvent> {
191 let mut events = Vec::new();
192
193 for (index, event) in ball_carry.carry_events().iter().enumerate() {
194 let kind = match event.kind {
195 BallCarryKind::Carry => "ball_carry",
196 BallCarryKind::AirDribble => "air_dribble",
197 };
198 events.push(span_mechanic_event(
199 kind,
200 index,
201 event.start_frame,
202 event.end_frame,
203 event.start_time,
204 event.end_time,
205 event.player_id.clone(),
206 event.is_team_0,
207 ));
208 }
209
210 for (index, event) in ceiling_shot.events().iter().enumerate() {
211 events.push(span_mechanic_event(
212 "ceiling_shot",
213 index,
214 event.ceiling_contact_frame,
215 event.frame,
216 event.ceiling_contact_time,
217 event.time,
218 event.player.clone(),
219 event.is_team_0,
220 ));
221 }
222
223 for (index, event) in dodge_reset.on_ball_events().iter().enumerate() {
224 events.push(moment_mechanic_event(
225 "flip_reset",
226 index,
227 event.frame,
228 event.time,
229 event.player.clone(),
230 event.is_team_0,
231 ));
232 }
233
234 for (index, event) in double_tap.events().iter().enumerate() {
235 events.push(span_mechanic_event(
236 "double_tap",
237 index,
238 event.backboard_frame,
239 event.frame,
240 event.backboard_time,
241 event.time,
242 event.player.clone(),
243 event.is_team_0,
244 ));
245 }
246
247 for (index, event) in flick.events().iter().enumerate() {
248 events.push(span_mechanic_event(
249 "flick",
250 index,
251 event.setup_start_frame,
252 event.frame,
253 event.setup_start_time,
254 event.time,
255 event.player.clone(),
256 event.is_team_0,
257 ));
258 }
259
260 for (index, event) in musty_flick.events().iter().enumerate() {
261 events.push(span_mechanic_event(
262 "musty_flick",
263 index,
264 event.dodge_frame,
265 event.frame,
266 event.dodge_time,
267 event.time,
268 event.player.clone(),
269 event.is_team_0,
270 ));
271 }
272
273 for (index, event) in one_timer.events().iter().enumerate() {
274 events.push(span_mechanic_event(
275 "one_timer",
276 index,
277 event.pass_start_frame,
278 event.frame,
279 event.pass_start_time,
280 event.time,
281 event.player.clone(),
282 event.is_team_0,
283 ));
284 }
285
286 for (index, event) in pass.events().iter().enumerate() {
287 events.push(span_mechanic_event(
288 "pass",
289 index,
290 event.start_frame,
291 event.frame,
292 event.start_time,
293 event.time,
294 event.passer.clone(),
295 event.is_team_0,
296 ));
297 }
298
299 for (index, event) in speed_flip.events().iter().enumerate() {
300 events.push(moment_mechanic_event(
301 "speed_flip",
302 index,
303 event.frame,
304 event.time,
305 event.player.clone(),
306 event.is_team_0,
307 ));
308 }
309
310 for (index, event) in half_flip.events().iter().enumerate() {
311 events.push(moment_mechanic_event(
312 "half_flip",
313 index,
314 event.frame,
315 event.time,
316 event.player.clone(),
317 event.is_team_0,
318 ));
319 }
320
321 for (index, event) in wavedash.events().iter().enumerate() {
322 events.push(span_mechanic_event(
323 "wavedash",
324 index,
325 event.dodge_frame,
326 event.frame,
327 event.dodge_time,
328 event.time,
329 event.player.clone(),
330 event.is_team_0,
331 ));
332 }
333
334 events.sort_by(|left, right| {
335 let left_time = mechanic_event_start_time(left);
336 let right_time = mechanic_event_start_time(right);
337 left_time
338 .total_cmp(&right_time)
339 .then_with(|| left.kind.cmp(&right.kind))
340 .then_with(|| left.id.cmp(&right.id))
341 });
342 events
343}
344
345fn mechanic_event_start_time(event: &MechanicEvent) -> f32 {
346 match event.timing {
347 MechanicTiming::Moment { time, .. } => time,
348 MechanicTiming::Span { start_time, .. } => start_time,
349 }
350}