Skip to main content

subtr_actor/stats/analysis_graph/nodes/
stats_timeline_frame.rs

1use super::*;
2use crate::stats::calculators::*;
3use crate::*;
4
5/// Holds the materialized per-frame stats snapshot DTO.
6#[derive(Debug, Clone, Default)]
7pub struct StatsTimelineFrameState {
8    pub frame: Option<ReplayStatsFrame>,
9}
10
11/// Terminal materialization node for the full stats timeline frame export.
12///
13/// This node aggregates many concrete calculator states into the typed
14/// `ReplayStatsFrame` DTO for serialization and UI/client compatibility. It is
15/// not a shared data provider for other analysis nodes; cross-node data flow
16/// should stay on explicit dependencies on the specific upstream calculator or
17/// state node.
18pub struct StatsTimelineFrameNode {
19    replay_meta: Option<ReplayMeta>,
20    state: StatsTimelineFrameState,
21}
22
23impl StatsTimelineFrameNode {
24    pub fn new() -> Self {
25        Self {
26            replay_meta: None,
27            state: StatsTimelineFrameState::default(),
28        }
29    }
30
31    fn replay_meta(&self) -> SubtrActorResult<&ReplayMeta> {
32        self.replay_meta.as_ref().ok_or_else(|| {
33            SubtrActorError::new(SubtrActorErrorVariant::CallbackError(
34                "missing ReplayMeta state while building timeline frame".to_owned(),
35            ))
36        })
37    }
38
39    fn is_team_zero_player(replay_meta: &ReplayMeta, player: &PlayerInfo) -> bool {
40        replay_meta
41            .team_zero
42            .iter()
43            .any(|team_player| team_player.remote_id == player.remote_id)
44    }
45
46    fn team_snapshot(
47        &self,
48        ctx: &AnalysisStateContext<'_>,
49        is_team_zero: bool,
50    ) -> SubtrActorResult<TeamStatsSnapshot> {
51        let projection = ctx.get::<StatsProjectionState>()?;
52        Ok(TeamStatsSnapshot {
53            fifty_fifty: projection.fifty_fifty.stats().for_team(is_team_zero),
54            kickoff: projection.kickoff.stats().for_team(is_team_zero),
55            possession: projection.possession.stats().for_team(is_team_zero),
56            ball_half: projection.ball_half.stats().for_team(is_team_zero),
57            ball_third: projection.ball_third.stats().for_team(is_team_zero),
58            territorial_pressure: projection
59                .territorial_pressure
60                .stats()
61                .for_team(is_team_zero),
62            rotation: if is_team_zero {
63                projection.rotation.team_zero_stats().clone()
64            } else {
65                projection.rotation.team_one_stats().clone()
66            },
67            rush: projection.rush.stats().for_team(is_team_zero),
68            core: if is_team_zero {
69                projection.core.team_zero_stats()
70            } else {
71                projection.core.team_one_stats()
72            },
73            backboard: if is_team_zero {
74                projection.backboard.team_zero_stats().clone()
75            } else {
76                projection.backboard.team_one_stats().clone()
77            },
78            double_tap: if is_team_zero {
79                projection.double_tap.team_zero_stats().clone()
80            } else {
81                projection.double_tap.team_one_stats().clone()
82            },
83            one_timer: if is_team_zero {
84                projection.one_timer.team_zero_stats().clone()
85            } else {
86                projection.one_timer.team_one_stats().clone()
87            },
88            pass: if is_team_zero {
89                projection.pass.team_zero_stats().clone()
90            } else {
91                projection.pass.team_one_stats().clone()
92            },
93            ball_carry: if is_team_zero {
94                projection.ball_carry.team_zero_stats().clone()
95            } else {
96                projection.ball_carry.team_one_stats().clone()
97            },
98            controlled_play: if is_team_zero {
99                projection.controlled_play.team_zero_stats().clone()
100            } else {
101                projection.controlled_play.team_one_stats().clone()
102            },
103            air_dribble: if is_team_zero {
104                projection.ball_carry.team_zero_air_dribble_stats().clone()
105            } else {
106                projection.ball_carry.team_one_air_dribble_stats().clone()
107            },
108            boost: if is_team_zero {
109                projection.boost.team_zero_stats().clone()
110            } else {
111                projection.boost.team_one_stats().clone()
112            },
113            bump: if is_team_zero {
114                projection.bump.team_zero_stats().clone()
115            } else {
116                projection.bump.team_one_stats().clone()
117            },
118            half_volley: if is_team_zero {
119                projection.half_volley.team_zero_stats().clone()
120            } else {
121                projection.half_volley.team_one_stats().clone()
122            },
123            movement: if is_team_zero {
124                projection.movement.team_zero_stats().clone()
125            } else {
126                projection.movement.team_one_stats().clone()
127            },
128            positioning: if is_team_zero {
129                projection.positioning.team_zero_stats().clone()
130            } else {
131                projection.positioning.team_one_stats().clone()
132            },
133            powerslide: if is_team_zero {
134                projection.powerslide.team_zero_stats().clone()
135            } else {
136                projection.powerslide.team_one_stats().clone()
137            },
138            demo: if is_team_zero {
139                projection.demo.team_zero_stats().clone()
140            } else {
141                projection.demo.team_one_stats().clone()
142            },
143        })
144    }
145
146    fn player_snapshot(
147        &self,
148        ctx: &AnalysisStateContext<'_>,
149        replay_meta: &ReplayMeta,
150        player: &PlayerInfo,
151    ) -> SubtrActorResult<PlayerStatsSnapshot> {
152        let player_id = &player.remote_id;
153        let projection = ctx.get::<StatsProjectionState>()?;
154        Ok(PlayerStatsSnapshot {
155            player_id: player.remote_id.clone(),
156            name: player.name.clone(),
157            is_team_0: Self::is_team_zero_player(replay_meta, player),
158            core: projection
159                .core
160                .player_stats()
161                .get(player_id)
162                .cloned()
163                .unwrap_or_default(),
164            backboard: projection
165                .backboard
166                .player_stats()
167                .get(player_id)
168                .cloned()
169                .unwrap_or_default(),
170            ceiling_shot: projection
171                .ceiling_shot
172                .player_stats()
173                .get(player_id)
174                .cloned()
175                .unwrap_or_default(),
176            wall_aerial: projection
177                .wall_aerial
178                .player_stats()
179                .get(player_id)
180                .cloned()
181                .unwrap_or_default(),
182            wall_aerial_shot: projection
183                .wall_aerial_shot
184                .player_stats()
185                .get(player_id)
186                .cloned()
187                .unwrap_or_default(),
188            double_tap: projection
189                .double_tap
190                .player_stats()
191                .get(player_id)
192                .cloned()
193                .unwrap_or_default(),
194            one_timer: projection
195                .one_timer
196                .player_stats()
197                .get(player_id)
198                .cloned()
199                .unwrap_or_default(),
200            pass: projection
201                .pass
202                .player_stats()
203                .get(player_id)
204                .cloned()
205                .unwrap_or_default(),
206            fifty_fifty: projection
207                .fifty_fifty
208                .player_stats()
209                .get(player_id)
210                .cloned()
211                .unwrap_or_default(),
212            kickoff: projection
213                .kickoff
214                .player_stats()
215                .get(player_id)
216                .cloned()
217                .unwrap_or_default(),
218            speed_flip: projection
219                .speed_flip
220                .player_stats()
221                .get(player_id)
222                .cloned()
223                .unwrap_or_default(),
224            half_flip: projection
225                .half_flip
226                .player_stats()
227                .get(player_id)
228                .cloned()
229                .unwrap_or_default(),
230            wavedash: projection
231                .wavedash
232                .player_stats()
233                .get(player_id)
234                .cloned()
235                .unwrap_or_default(),
236            touch: projection
237                .touch
238                .player_stats()
239                .get(player_id)
240                .cloned()
241                .unwrap_or_default(),
242            whiff: projection
243                .whiff
244                .player_stats()
245                .get(player_id)
246                .cloned()
247                .unwrap_or_default(),
248            flick: projection
249                .flick
250                .player_stats()
251                .get(player_id)
252                .cloned()
253                .unwrap_or_default(),
254            dodge_reset: projection
255                .dodge_reset
256                .player_stats()
257                .get(player_id)
258                .cloned()
259                .unwrap_or_default(),
260            flip_reset: projection
261                .flip_reset
262                .player_stats()
263                .get(player_id)
264                .cloned()
265                .unwrap_or_default(),
266            ball_carry: projection
267                .ball_carry
268                .player_stats()
269                .get(player_id)
270                .cloned()
271                .unwrap_or_default(),
272            controlled_play: projection
273                .controlled_play
274                .player_stats()
275                .get(player_id)
276                .cloned()
277                .unwrap_or_default(),
278            air_dribble: projection
279                .ball_carry
280                .player_air_dribble_stats()
281                .get(player_id)
282                .cloned()
283                .unwrap_or_default(),
284            boost: projection
285                .boost
286                .player_stats()
287                .get(player_id)
288                .cloned()
289                .unwrap_or_default(),
290            bump: projection
291                .bump
292                .player_stats()
293                .get(player_id)
294                .cloned()
295                .unwrap_or_default(),
296            half_volley: projection
297                .half_volley
298                .player_stats()
299                .get(player_id)
300                .cloned()
301                .unwrap_or_default(),
302            movement: projection
303                .movement
304                .player_stats()
305                .get(player_id)
306                .cloned()
307                .unwrap_or_default(),
308            positioning: projection
309                .positioning
310                .player_stats()
311                .get(player_id)
312                .cloned()
313                .unwrap_or_default(),
314            rotation: projection
315                .rotation
316                .player_stats()
317                .get(player_id)
318                .cloned()
319                .unwrap_or_default(),
320            powerslide: projection
321                .powerslide
322                .player_stats()
323                .get(player_id)
324                .cloned()
325                .unwrap_or_default(),
326            demo: projection
327                .demo
328                .player_stats()
329                .get(player_id)
330                .cloned()
331                .unwrap_or_default(),
332        })
333    }
334
335    fn update_snapshot(&mut self, ctx: &AnalysisStateContext<'_>) -> SubtrActorResult<()> {
336        let replay_meta = self.replay_meta()?;
337        let frame = ctx.get::<FrameInfo>()?;
338        let gameplay = ctx.get::<GameplayState>()?;
339        let live_play_state = ctx.get::<LivePlayState>()?;
340        self.state.frame = Some(ReplayStatsFrame {
341            frame_number: frame.frame_number,
342            time: frame.time,
343            dt: frame.dt,
344            seconds_remaining: frame.seconds_remaining,
345            game_state: gameplay.game_state,
346            ball_has_been_hit: gameplay.ball_has_been_hit,
347            kickoff_countdown_time: gameplay.kickoff_countdown_time,
348            gameplay_phase: live_play_state.gameplay_phase,
349            is_live_play: live_play_state.is_live_play,
350            team_zero: self.team_snapshot(ctx, true)?,
351            team_one: self.team_snapshot(ctx, false)?,
352            players: replay_meta
353                .player_order()
354                .map(|player| self.player_snapshot(ctx, replay_meta, player))
355                .collect::<SubtrActorResult<Vec<_>>>()?,
356        });
357        Ok(())
358    }
359}
360
361impl Default for StatsTimelineFrameNode {
362    fn default() -> Self {
363        Self::new()
364    }
365}
366
367impl AnalysisNode for StatsTimelineFrameNode {
368    type State = StatsTimelineFrameState;
369
370    fn name(&self) -> &'static str {
371        "stats_timeline_frame"
372    }
373
374    fn on_replay_meta(&mut self, meta: &ReplayMeta) -> SubtrActorResult<()> {
375        self.replay_meta = Some(meta.clone());
376        Ok(())
377    }
378
379    fn dependencies(&self) -> NodeDependencies {
380        vec![
381            frame_info_dependency(),
382            gameplay_state_dependency(),
383            live_play_dependency(),
384            stats_projection_dependency(),
385        ]
386    }
387
388    fn evaluate(&mut self, ctx: &AnalysisStateContext<'_>) -> SubtrActorResult<()> {
389        self.update_snapshot(ctx)
390    }
391
392    fn finish(&mut self, ctx: &AnalysisStateContext<'_>) -> SubtrActorResult<()> {
393        self.update_snapshot(ctx)
394    }
395
396    fn state(&self) -> &Self::State {
397        &self.state
398    }
399}