1+ use crate :: Result ;
2+ use crate :: scoring:: { ScoringEngine , RankingTitle } ;
3+ use crate :: game:: typing_animation:: { TypingAnimation , AnimationPhase } ;
4+ use crossterm:: event:: { self , Event , KeyCode } ;
5+ use ratatui:: {
6+ backend:: CrosstermBackend ,
7+ layout:: { Alignment , Constraint , Direction , Layout } ,
8+ style:: Style ,
9+ text:: { Line , Span , Text } ,
10+ widgets:: Paragraph ,
11+ Terminal , Frame ,
12+ } ;
13+ use std:: io;
14+
15+ pub struct AnimationScreen ;
16+
17+ impl AnimationScreen {
18+ // Helper function to convert crossterm::Color to ratatui::Color
19+ fn convert_crossterm_color ( color : crossterm:: style:: Color ) -> ratatui:: style:: Color {
20+ match color {
21+ crossterm:: style:: Color :: Black => ratatui:: style:: Color :: Black ,
22+ crossterm:: style:: Color :: DarkGrey => ratatui:: style:: Color :: DarkGray ,
23+ crossterm:: style:: Color :: Red => ratatui:: style:: Color :: Red ,
24+ crossterm:: style:: Color :: DarkRed => ratatui:: style:: Color :: DarkGray ,
25+ crossterm:: style:: Color :: Green => ratatui:: style:: Color :: Green ,
26+ crossterm:: style:: Color :: DarkGreen => ratatui:: style:: Color :: DarkGray ,
27+ crossterm:: style:: Color :: Yellow => ratatui:: style:: Color :: Yellow ,
28+ crossterm:: style:: Color :: DarkYellow => ratatui:: style:: Color :: DarkGray ,
29+ crossterm:: style:: Color :: Blue => ratatui:: style:: Color :: Blue ,
30+ crossterm:: style:: Color :: DarkBlue => ratatui:: style:: Color :: DarkGray ,
31+ crossterm:: style:: Color :: Magenta => ratatui:: style:: Color :: Magenta ,
32+ crossterm:: style:: Color :: DarkMagenta => ratatui:: style:: Color :: DarkGray ,
33+ crossterm:: style:: Color :: Cyan => ratatui:: style:: Color :: Cyan ,
34+ crossterm:: style:: Color :: DarkCyan => ratatui:: style:: Color :: DarkGray ,
35+ crossterm:: style:: Color :: White => ratatui:: style:: Color :: White ,
36+ crossterm:: style:: Color :: Grey => ratatui:: style:: Color :: Gray ,
37+ _ => ratatui:: style:: Color :: White , // Default fallback
38+ }
39+ }
40+
41+ // Helper function to render typing animation with ratatui
42+ fn render_typing_animation_ratatui ( frame : & mut Frame , animation : & TypingAnimation , _ranking_title : & str ) {
43+ let area = frame. size ( ) ;
44+
45+ // Create vertical layout for centering
46+ let chunks = Layout :: default ( )
47+ . direction ( Direction :: Vertical )
48+ . constraints ( [
49+ Constraint :: Percentage ( 40 ) , // Top padding
50+ Constraint :: Min ( 4 ) , // Animation area
51+ Constraint :: Percentage ( 40 ) , // Bottom padding
52+ ] )
53+ . split ( area) ;
54+
55+ match animation. get_current_phase ( ) {
56+ AnimationPhase :: ConcentrationLines => {
57+ let mut lines = Vec :: new ( ) ;
58+
59+ for ( i, line) in animation. get_hacking_lines ( ) . iter ( ) . enumerate ( ) {
60+ let text = & line. text [ ..line. typed_length ] ;
61+ let line_color = Self :: convert_crossterm_color ( line. color ) ;
62+
63+ if i == animation. get_current_line ( ) && line. typed_length < line. text . len ( ) && !line. completed {
64+ // Show cursor on current line
65+ lines. push ( Line :: from ( vec ! [
66+ Span :: styled( text, Style :: default ( ) . fg( line_color) ) ,
67+ Span :: styled( "█" , Style :: default ( ) . fg( ratatui:: style:: Color :: White ) ) ,
68+ ] ) ) ;
69+ } else if !text. is_empty ( ) {
70+ // Regular completed or typing line
71+ lines. push ( Line :: from (
72+ Span :: styled ( text, Style :: default ( ) . fg ( line_color) )
73+ ) ) ;
74+ } else {
75+ // Empty placeholder line
76+ lines. push ( Line :: from ( "" ) ) ;
77+ }
78+ }
79+
80+ let paragraph = Paragraph :: new ( Text :: from ( lines) )
81+ . alignment ( Alignment :: Center ) ;
82+
83+ frame. render_widget ( paragraph, chunks[ 1 ] ) ;
84+
85+ // Render skip hint in bottom right
86+ Self :: render_skip_hint ( frame, area) ;
87+ }
88+ AnimationPhase :: Pause => {
89+ // Show all completed lines plus dots
90+ let mut lines = Vec :: new ( ) ;
91+
92+ for line in animation. get_hacking_lines ( ) . iter ( ) {
93+ let line_color = Self :: convert_crossterm_color ( line. color ) ;
94+ lines. push ( Line :: from (
95+ Span :: styled ( & line. text , Style :: default ( ) . fg ( line_color) )
96+ ) ) ;
97+ }
98+
99+ // Add dots line
100+ let dots = "." . repeat ( animation. get_pause_dots ( ) ) ;
101+ lines. push ( Line :: from (
102+ Span :: styled ( dots, Style :: default ( ) . fg ( ratatui:: style:: Color :: Gray ) )
103+ ) ) ;
104+
105+ let paragraph = Paragraph :: new ( Text :: from ( lines) )
106+ . alignment ( Alignment :: Center ) ;
107+
108+ frame. render_widget ( paragraph, chunks[ 1 ] ) ;
109+
110+ // Render skip hint in bottom right
111+ Self :: render_skip_hint ( frame, area) ;
112+ }
113+ AnimationPhase :: Complete => {
114+ // Animation is complete, ready to transition to result
115+ }
116+ }
117+ }
118+
119+ // Helper function to render skip hint in bottom right corner
120+ fn render_skip_hint ( frame : & mut Frame , area : ratatui:: layout:: Rect ) {
121+ let skip_text = "[S] Skip" ;
122+ let skip_width = skip_text. len ( ) as u16 ;
123+ let skip_height = 1 ;
124+
125+ // Position in bottom right corner with small margin
126+ let skip_x = area. width . saturating_sub ( skip_width + 1 ) ;
127+ let skip_y = area. height . saturating_sub ( skip_height + 1 ) ;
128+
129+ let skip_area = ratatui:: layout:: Rect {
130+ x : skip_x,
131+ y : skip_y,
132+ width : skip_width,
133+ height : skip_height,
134+ } ;
135+
136+ let skip_paragraph = Paragraph :: new ( skip_text)
137+ . style ( Style :: default ( ) . fg ( ratatui:: style:: Color :: Gray ) ) ;
138+
139+ frame. render_widget ( skip_paragraph, skip_area) ;
140+ }
141+
142+ // Helper function to get tier from ranking title name
143+ fn get_tier_from_title ( title_name : & str ) -> crate :: scoring:: RankingTier {
144+ RankingTitle :: all_titles ( )
145+ . iter ( )
146+ . find ( |title| title. name ( ) == title_name)
147+ . map ( |title| title. tier ( ) . clone ( ) )
148+ . unwrap_or ( crate :: scoring:: RankingTier :: Beginner )
149+ }
150+
151+ pub fn show_session_animation (
152+ _total_stages : usize ,
153+ _completed_stages : usize ,
154+ stage_engines : & [ ( String , ScoringEngine ) ] ,
155+ ) -> Result < ( ) > {
156+ // Calculate aggregated session metrics by combining ScoringEngines with + operator
157+ if stage_engines. is_empty ( ) {
158+ return Ok ( ( ) ) ;
159+ }
160+
161+ let combined_engine = stage_engines. iter ( )
162+ . map ( |( _, engine) | engine. clone ( ) )
163+ . reduce ( |acc, engine| acc + engine)
164+ . unwrap ( ) ; // Safe because we checked is_empty() above
165+
166+ let session_metrics = match combined_engine. calculate_metrics ( ) {
167+ Ok ( metrics) => metrics,
168+ Err ( _) => {
169+ // Fallback if calculation fails
170+ return Ok ( ( ) ) ;
171+ }
172+ } ;
173+
174+ // Set up ratatui terminal
175+ let backend = CrosstermBackend :: new ( io:: stdout ( ) ) ;
176+ let mut terminal = Terminal :: new ( backend) ?;
177+ terminal. clear ( ) ?;
178+
179+ // Create typing animation for session complete
180+ let tier = Self :: get_tier_from_title ( & session_metrics. ranking_title ) ;
181+ let mut typing_animation = TypingAnimation :: new ( tier, terminal. size ( ) ?. width , terminal. size ( ) ?. height ) ;
182+ typing_animation. set_rank_messages ( & session_metrics. ranking_title ) ;
183+
184+ // Show typing reveal animation with ratatui
185+ while !typing_animation. is_complete ( ) {
186+ let updated = typing_animation. update ( ) ;
187+
188+ if updated {
189+ let ranking_title = session_metrics. ranking_title . clone ( ) ;
190+ terminal. draw ( |frame| {
191+ Self :: render_typing_animation_ratatui ( frame, & typing_animation, & ranking_title) ;
192+ } ) ?;
193+ }
194+
195+ // Check for S key to skip animation
196+ if event:: poll ( std:: time:: Duration :: from_millis ( 50 ) ) ? {
197+ if let Event :: Key ( key_event) = event:: read ( ) ? {
198+ match key_event. code {
199+ KeyCode :: Char ( 's' ) | KeyCode :: Char ( 'S' ) => {
200+ break ;
201+ }
202+ _ => {
203+ // Ignore other keys to prevent accidental skipping
204+ }
205+ }
206+ }
207+ }
208+
209+ std:: thread:: sleep ( std:: time:: Duration :: from_millis ( 16 ) ) ; // ~60fps
210+ }
211+
212+ Ok ( ( ) )
213+ }
214+ }
0 commit comments