1 #include "game.h" // IWYU pragma: associated
2 
3 #include <algorithm>
4 #include <map>
5 #include <string>
6 #include <vector>
7 
8 #include "avatar.h"
9 #include "calendar.h"
10 #include "color.h"
11 #include "debug.h"
12 #include "input.h"
13 #include "mission.h"
14 #include "npc.h"
15 #include "output.h"
16 #include "string_formatter.h"
17 #include "translations.h"
18 #include "ui.h"
19 #include "ui_manager.h"
20 
list_missions()21 void game::list_missions()
22 {
23     catacurses::window w_missions;
24 
25     enum class tab_mode : int {
26         TAB_ACTIVE = 0,
27         TAB_COMPLETED,
28         TAB_FAILED,
29         NUM_TABS,
30         FIRST_TAB = 0,
31         LAST_TAB = NUM_TABS - 1
32     };
33     tab_mode tab = tab_mode::FIRST_TAB;
34     size_t selection = 0;
35     int entries_per_page = 0;
36     input_context ctxt( "MISSIONS" );
37     ctxt.register_cardinal();
38     ctxt.register_action( "CONFIRM" );
39     ctxt.register_action( "QUIT" );
40     ctxt.register_action( "HELP_KEYBINDINGS" );
41 
42     ui_adaptor ui;
43     ui.on_screen_resize( [&]( ui_adaptor & ui ) {
44         w_missions = new_centered_win( FULL_SCREEN_HEIGHT, FULL_SCREEN_WIDTH );
45 
46         // content ranges from y=3 to FULL_SCREEN_HEIGHT - 2
47         entries_per_page = FULL_SCREEN_HEIGHT - 4;
48 
49         ui.position_from_window( w_missions );
50     } );
51     ui.mark_resize();
52 
53     std::vector<mission *> umissions;
54 
55     ui.on_redraw( [&]( const ui_adaptor & ) {
56         werase( w_missions );
57         // entries_per_page * page number
58         const int top_of_page = entries_per_page * ( selection / entries_per_page );
59         const int bottom_of_page =
60             std::min( top_of_page + entries_per_page - 1, static_cast<int>( umissions.size() ) - 1 );
61 
62         for( int i = 3; i < FULL_SCREEN_HEIGHT - 1; i++ ) {
63             mvwputch( w_missions, point( 30, i ), BORDER_COLOR, LINE_XOXO );
64         }
65 
66         const std::vector<std::pair<tab_mode, std::string>> tabs = {
67             { tab_mode::TAB_ACTIVE, _( "ACTIVE MISSIONS" ) },
68             { tab_mode::TAB_COMPLETED, _( "COMPLETED MISSIONS" ) },
69             { tab_mode::TAB_FAILED, _( "FAILED MISSIONS" ) },
70         };
71         draw_tabs( w_missions, tabs, tab );
72         draw_border_below_tabs( w_missions );
73         int x1 = 2, x2 = 2;
74         for( const std::pair<tab_mode, std::string> &t : tabs ) {
75             x2 = x1 + utf8_width( t.second ) + 1;
76             if( t.first == tab ) {
77                 break;
78             }
79             x1 = x2 + 2;
80         }
81         mvwputch( w_missions, point( 30, 2 ), BORDER_COLOR, x1 < 30 && 30 < x2 ? ' ' : LINE_OXXX ); // ^|^*/
82         mvwputch( w_missions, point( 30, FULL_SCREEN_HEIGHT - 1 ), BORDER_COLOR, LINE_XXOX ); // _|_
83 
84         draw_scrollbar( w_missions, selection, entries_per_page, umissions.size(), point( 0, 3 ) );
85 
86         for( int i = top_of_page; i <= bottom_of_page; i++ ) {
87             mission *miss = umissions[i];
88             const nc_color col = u.get_active_mission() == miss ? c_light_green : c_white;
89             const int y = i - top_of_page + 3;
90             trim_and_print( w_missions, point( 1, y ), 28,
91                             static_cast<int>( selection ) == i ? hilite( col ) : col,
92                             miss->name() );
93         }
94 
95         if( selection < umissions.size() ) {
96             mission *miss = umissions[selection];
97             const nc_color col = u.get_active_mission() == miss ? c_light_green : c_white;
98             std::string for_npc;
99             if( miss->get_npc_id().is_valid() ) {
100                 npc *guy = g->find_npc( miss->get_npc_id() );
101                 if( guy ) {
102                     for_npc = string_format( _( " for %s" ), guy->disp_name() );
103                 }
104             }
105 
106             int y = 3;
107 
108             auto format_tokenized_description = []( const std::string & description,
109             const std::vector<std::pair<int, itype_id>> &rewards ) {
110                 std::string formatted_description = description;
111                 for( const auto &reward : rewards ) {
112                     std::string token = "<reward_count:" + reward.second.str() + ">";
113                     formatted_description = string_replace( formatted_description, token, string_format( "%d",
114                                                             reward.first ) );
115                 }
116                 return formatted_description;
117             };
118             y += fold_and_print( w_missions, point( 31, y ), getmaxx( w_missions ) - 33, col,
119                                  format_tokenized_description( miss->name(), miss->get_likely_rewards() ) + for_npc );
120             y++;
121             if( !miss->get_description().empty() ) {
122                 y += fold_and_print( w_missions, point( 31, y ), getmaxx( w_missions ) - 33, c_white,
123                                      format_tokenized_description( miss->get_description(), miss->get_likely_rewards() ) );
124             }
125             if( miss->has_deadline() ) {
126                 const time_point deadline = miss->get_deadline();
127                 mvwprintz( w_missions, point( 31, ++y ), c_white, _( "Deadline: %s" ), to_string( deadline ) );
128 
129                 if( tab != tab_mode::TAB_COMPLETED ) {
130                     // There's no point in displaying this for a completed mission.
131                     // @ TODO: But displaying when you completed it would be useful.
132                     const time_duration remaining = deadline - calendar::turn;
133                     std::string remaining_time;
134 
135                     if( remaining <= 0_turns ) {
136                         remaining_time = _( "None!" );
137                     } else if( u.has_watch() ) {
138                         remaining_time = to_string( remaining );
139                     } else {
140                         remaining_time = to_string_approx( remaining );
141                     }
142 
143                     mvwprintz( w_missions, point( 31, ++y ), c_white, _( "Time remaining: %s" ), remaining_time );
144                 }
145             }
146             if( miss->has_target() ) {
147                 const tripoint_abs_omt pos = u.global_omt_location();
148                 // TODO: target does not contain a z-component, targets are assumed to be on z=0
149                 mvwprintz( w_missions, point( 31, ++y ), c_white, _( "Target: %s   You: %s" ),
150                            miss->get_target().to_string(), pos.to_string() );
151             }
152         } else {
153             static const std::map< tab_mode, std::string > nope = {
154                 { tab_mode::TAB_ACTIVE, translate_marker( "You have no active missions!" ) },
155                 { tab_mode::TAB_COMPLETED, translate_marker( "You haven't completed any missions!" ) },
156                 { tab_mode::TAB_FAILED, translate_marker( "You haven't failed any missions!" ) }
157             };
158             mvwprintz( w_missions, point( 31, 4 ), c_light_red, _( nope.at( tab ) ) );
159         }
160 
161         wnoutrefresh( w_missions );
162     } );
163 
164     while( true ) {
165         umissions.clear();
166         if( tab < tab_mode::FIRST_TAB || tab >= tab_mode::NUM_TABS ) {
167             debugmsg( "The sanity check failed because tab=%d", static_cast<int>( tab ) );
168             tab = tab_mode::FIRST_TAB;
169         }
170         switch( tab ) {
171             case tab_mode::TAB_ACTIVE:
172                 umissions = u.get_active_missions();
173                 break;
174             case tab_mode::TAB_COMPLETED:
175                 umissions = u.get_completed_missions();
176                 break;
177             case tab_mode::TAB_FAILED:
178                 umissions = u.get_failed_missions();
179                 break;
180             default:
181                 break;
182         }
183         if( ( !umissions.empty() && selection >= umissions.size() ) ||
184             ( umissions.empty() && selection != 0 ) ) {
185             debugmsg( "Sanity check failed: selection=%d, size=%d", static_cast<int>( selection ),
186                       static_cast<int>( umissions.size() ) );
187             selection = 0;
188         }
189         ui_manager::redraw();
190         const std::string action = ctxt.handle_input();
191         if( action == "RIGHT" ) {
192             tab = static_cast<tab_mode>( static_cast<int>( tab ) + 1 );
193             if( tab >= tab_mode::NUM_TABS ) {
194                 tab = tab_mode::FIRST_TAB;
195             }
196             selection = 0;
197         } else if( action == "LEFT" ) {
198             tab = static_cast<tab_mode>( static_cast<int>( tab ) - 1 );
199             if( tab < tab_mode::FIRST_TAB ) {
200                 tab = tab_mode::LAST_TAB;
201             }
202             selection = 0;
203         } else if( action == "DOWN" ) {
204             selection++;
205             if( selection >= umissions.size() ) {
206                 selection = 0;
207             }
208         } else if( action == "UP" ) {
209             if( selection == 0 ) {
210                 selection = umissions.empty() ? 0 : umissions.size() - 1;
211             } else {
212                 selection--;
213             }
214         } else if( action == "CONFIRM" ) {
215             if( tab == tab_mode::TAB_ACTIVE && selection < umissions.size() ) {
216                 u.set_active_mission( *umissions[selection] );
217             }
218             break;
219         } else if( action == "QUIT" ) {
220             break;
221         }
222     }
223 }
224