Skip to main content

subtr_actor/stats/analysis_graph/nodes/
stats_timeline_frame.rs

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