std::string to_string( const time_duration &d ) { if( d >= time_duration::from_turns( calendar::INDEFINITELY_LONG ) ) { return _( "forever" ); } if( d <= 1_minutes ) { return to_string_clipped( d ); } time_duration divider = 0_turns; if( d < 1_hours ) { divider = 1_minutes; } else if( d < 1_days ) { divider = 1_hours; } else { divider = 24_hours; } if( d % divider != 0_turns ) { //~ %1$s - greater units of time (e.g. 3 hours), %2$s - lesser units of time (e.g. 11 minutes). return string_format( _( "%1$s and %2$s" ), to_string_clipped( d ), to_string_clipped( d % divider ) ); } return to_string_clipped( d ); }
std::string to_string( const time_duration &d ) { const int turns = to_turns<int>( d ); int divider = 0; if( turns > MINUTES( 1 ) && turns < calendar::INDEFINITELY_LONG ) { if( turns < HOURS( 1 ) ) { divider = MINUTES( 1 ); } else if( turns < DAYS( 1 ) ) { divider = HOURS( 1 ); } else { divider = DAYS( 1 ); } } const int remainder = divider ? turns % divider : 0; if( remainder != 0 ) { //~ %1$s - greater units of time (e.g. 3 hours), %2$s - lesser units of time (e.g. 11 minutes). return string_format( _( "%1$s and %2$s" ), to_string_clipped( time_duration::from_turns( turns ) ), to_string_clipped( time_duration::from_turns( remainder ) ) ); } return to_string_clipped( d ); }
comestible_inventory_preset( const player &p ) : inventory_selector_preset(), p( p ) { append_cell( [ p, this ]( const item_location & loc ) { return good_bad_none( p.nutrition_for( get_comestible_item( loc ) ) ); }, _( "NUTRITION" ) ); append_cell( [ this ]( const item_location & loc ) { return good_bad_none( get_edible_comestible( loc ).quench ); }, _( "QUENCH" ) ); append_cell( [ p, this ]( const item_location & loc ) { return good_bad_none( p.fun_for( get_comestible_item( loc ) ).first ); }, _( "JOY" ) ); append_cell( [ this ]( const item_location & loc ) { const int spoils = get_edible_comestible( loc ).spoils; if( spoils > 0 ) { return to_string_clipped( time_duration::from_turns( spoils ) ); } return std::string(); }, _( "SPOILS IN" ) ); append_cell( [ this, &p ]( const item_location & loc ) { std::string cbm_name; switch( p.get_cbm_rechargeable_with( get_comestible_item( loc ) ) ) { case rechargeable_cbm::none: break; case rechargeable_cbm::battery: cbm_name = _( "Battery" ); break; case rechargeable_cbm::reactor: cbm_name = _( "Reactor" ); break; case rechargeable_cbm::furnace: cbm_name = _( "Furnace" ); break; } if( !cbm_name.empty() ) { return string_format( "<color_cyan>%s</color>", cbm_name.c_str() ); } return std::string(); }, _( "CBM" ) ); append_cell( [ this, &p ]( const item_location & loc ) { return good_bad_none( p.get_acquirable_energy( get_comestible_item( loc ) ) ); }, _( "ENERGY" ) ); }
disassemble_inventory_preset( const player &p, const inventory &inv ) : pickup_inventory_preset( p ), p( p ), inv( inv ) { append_cell( [ this ]( const item_location & loc ) { const auto &req = get_recipe( loc ).disassembly_requirements(); if( req.is_empty() ) { return std::string(); } const auto components = req.get_components(); return enumerate_as_string( components.begin(), components.end(), []( const decltype( components )::value_type & comps ) { return comps.front().to_string(); } ); }, _( "YIELD" ) ); append_cell( [ this ]( const item_location & loc ) { return to_string_clipped( time_duration::from_turns( get_recipe( loc ).time / 100 ) ); }, _( "TIME" ) ); }
std::string to_string_approx( const time_duration &dur, const bool verbose ) { time_duration d = dur; const auto make_result = [verbose]( const time_duration & d, const char *verbose_str, const char *short_str ) { return string_format( verbose ? verbose_str : short_str, to_string_clipped( d ) ); }; time_duration divider = 0_turns; time_duration vicinity = 0_turns; if( d > 1_days ) { divider = 1_days; vicinity = 2_hours; } else if( d > 1_hours ) { divider = 1_hours; vicinity = 5_minutes; } // Minutes and seconds can be estimated precisely. if( divider != 0_turns ) { const time_duration remainder = d % divider; if( remainder >= divider - vicinity ) { d += divider; } else if( remainder > vicinity ) { if( remainder < divider / 2 ) { //~ %s - time (e.g. 2 hours). return make_result( d, _( "more than %s" ), ">%s" ); } else { //~ %s - time (e.g. 2 hours). return make_result( d + divider, _( "less than %s" ), "<%s" ); } } } //~ %s - time (e.g. 2 hours). return make_result( d, _( "about %s" ), "%s" ); }
std::string to_string_approx( const time_duration &d, const bool verbose ) { int turns = to_turns<int>( d ); const auto make_result = [verbose]( int turns, const char *verbose_str, const char *short_str ) { return string_format( verbose ? verbose_str : short_str, to_string_clipped( time_duration::from_turns( turns ) ) ); }; int divider = 0; int vicinity = 0; if( turns > DAYS( 1 ) ) { divider = DAYS( 1 ); vicinity = HOURS( 2 ); } else if( turns > HOURS( 1 ) ) { divider = HOURS( 1 ); vicinity = MINUTES( 5 ); } // Minutes and seconds can be estimated precisely. if( divider != 0 ) { const int remainder = turns % divider; if( remainder >= divider - vicinity ) { turns += divider; } else if( remainder > vicinity ) { if( remainder < divider / 2 ) { //~ %s - time (e.g. 2 hours). return make_result( turns, _( "more than %s" ), ">%s" ); } else { //~ %s - time (e.g. 2 hours). return make_result( turns + divider, _( "less than %s" ), "<%s" ); } } } //~ %s - time (e.g. 2 hours). return make_result( turns, _( "about %s" ), "%s" ); }
std::string to_string_clipped( const time_duration &d, const clipped_align align ) { const std::pair<int, clipped_unit> time = clipped_time( d ); return to_string_clipped( time.first, time.second, align ); }
void Messages::dialog::show() { werase( w ); draw_border( w, border_color ); scrollbar() .offset_x( 0 ) .offset_y( border_width ) .content_size( folded_filtered.size() ) .viewport_pos( offset ) .viewport_size( max_lines ) .apply( w ); // Range of window lines to print size_t line_from = 0, line_to; if( offset < folded_filtered.size() ) { line_to = std::min( max_lines, folded_filtered.size() - offset ); } else { line_to = 0; } if( !log_from_top ) { // Always print from new to old std::swap( line_from, line_to ); } std::string prev_time_str; bool printing_range = false; for( size_t line = line_from; line != line_to; ) { // Decrement here if printing from bottom to get the correct line number if( !log_from_top ) { --line; } const size_t folded_ind = offset + line; const size_t msg_ind = folded_all[folded_filtered[folded_ind]].first; const game_message &msg = player_messages.history( msg_ind ); nc_color col = msgtype_to_color( msg.type, false ); // Print current line print_colored_text( w, border_width + line, border_width + time_width + padding_width, col, col, folded_all[folded_filtered[folded_ind]].second ); // Generate aligned time string const time_point msg_time = msg.timestamp_in_turns; const std::string time_str = to_string_clipped( calendar::turn - msg_time, clipped_align::right ); if( time_str != prev_time_str ) { // Time changed, print time string prev_time_str = time_str; right_print( w, border_width + line, border_width + msg_width + padding_width, time_color, time_str ); printing_range = false; } else { // Print line brackets to mark ranges of time if( printing_range ) { const size_t last_line = log_from_top ? line - 1 : line + 1; wattron( w, bracket_color ); mvwaddch( w, border_width + last_line, border_width + time_width - 1, LINE_XOXO ); wattroff( w, bracket_color ); } wattron( w, bracket_color ); mvwaddch( w, border_width + line, border_width + time_width - 1, log_from_top ? LINE_XXOO : LINE_OXXO ); wattroff( w, bracket_color ); printing_range = true; } // Decrement for !log_from_top is done at the beginning if( log_from_top ) { ++line; } } if( filtering ) { wrefresh( w ); // Print the help text werase( w_filter_help ); draw_border( w_filter_help, border_color ); for( size_t line = 0; line < help_text.size(); ++line ) { nc_color col = filter_help_color; print_colored_text( w_filter_help, border_width + line, border_width, col, col, help_text[line] ); } mvwprintz( w_filter_help, w_fh_height - 1, border_width, border_color, "< " ); mvwprintz( w_filter_help, w_fh_height - 1, w_fh_width - border_width - 2, border_color, " >" ); wrefresh( w_filter_help ); // This line is preventing this method from being const filter.query( false, true ); // Draw only } else { if( filter_str.empty() ) { mvwprintz( w, w_height - 1, border_width, border_color, _( "< Press %s to filter, %s to reset >" ), ctxt.get_desc( "FILTER" ), ctxt.get_desc( "RESET_FILTER" ) ); } else { mvwprintz( w, w_height - 1, border_width, border_color, "< %s >", filter_str ); mvwprintz( w, w_height - 1, border_width + 2, filter_color, "%s", filter_str ); } wrefresh( w ); } }
void Messages::dialog::init() { w_width = std::min( TERMX, FULL_SCREEN_WIDTH ); w_height = std::min( TERMY, FULL_SCREEN_HEIGHT ); w_x = ( TERMX - w_width ) / 2; w_y = ( TERMY - w_height ) / 2; w = catacurses::newwin( w_height, w_width, w_y, w_x ); ctxt = input_context( "MESSAGE_LOG" ); ctxt.register_action( "UP", _( "Scroll up" ) ); ctxt.register_action( "DOWN", _( "Scroll down" ) ); ctxt.register_action( "PAGE_UP" ); ctxt.register_action( "PAGE_DOWN" ); ctxt.register_action( "FILTER" ); ctxt.register_action( "RESET_FILTER" ); ctxt.register_action( "QUIT" ); ctxt.register_action( "HELP_KEYBINDINGS" ); // Calculate time string display width. The translated strings are expected to // be aligned, so we choose an arbitrary duration here to calculate the width. time_width = utf8_width( to_string_clipped( 1_turns, clipped_align::right ) ); if( border_width * 2 + time_width + padding_width >= w_width || border_width * 2 >= w_height ) { debugmsg( "No enough space for the message window" ); errored = true; return; } msg_width = w_width - border_width * 2 - time_width - padding_width; max_lines = static_cast<size_t>( w_height - border_width * 2 ); // Initialize filter help text and window w_fh_width = w_width; w_fh_x = w_x; help_text = filter_help_text( w_fh_width - border_width * 2 ); w_fh_height = help_text.size() + border_width * 2; w_fh_y = w_y + w_height - w_fh_height; w_filter_help = catacurses::newwin( w_fh_height, w_fh_width, w_fh_y, w_fh_x ); // Initialize filter input filter.window( w_filter_help, border_width + 2, w_fh_height - 1, w_fh_width - border_width - 2 ); filtering = false; // Initialize folded messages folded_all.clear(); folded_filtered.clear(); const size_t msg_count = size(); for( size_t ind = 0; ind < msg_count; ++ind ) { const size_t msg_ind = log_from_top ? ind : msg_count - 1 - ind; const game_message &msg = player_messages.history( msg_ind ); const auto &folded = foldstring( msg.get_with_count(), msg_width ); for( const auto &it : folded ) { folded_filtered.emplace_back( folded_all.size() ); folded_all.emplace_back( msg_ind, it ); } } // Initialize scrolling offset if( log_from_top || max_lines > folded_filtered.size() ) { offset = 0; } else { offset = folded_filtered.size() - max_lines; } canceled = false; errored = false; }
void Messages::display_messages() { catacurses::window w = catacurses::newwin( FULL_SCREEN_HEIGHT, FULL_SCREEN_WIDTH, ( TERMY > FULL_SCREEN_HEIGHT ) ? ( TERMY - FULL_SCREEN_HEIGHT ) / 2 : 0, ( TERMX > FULL_SCREEN_WIDTH ) ? ( TERMX - FULL_SCREEN_WIDTH ) / 2 : 0 ); input_context ctxt( "MESSAGE_LOG" ); ctxt.register_action( "UP", _( "Scroll up" ) ); ctxt.register_action( "DOWN", _( "Scroll down" ) ); ctxt.register_action( "QUIT" ); ctxt.register_action( "HELP_KEYBINDINGS" ); /* Right-Aligning Time Epochs For Readability * ========================================== * Given display_messages(); right-aligns epochs, we must declare one quick variable first: * max_padlength refers to the length of the LONGEST possible unit of time returned by to_string_clipped() any language has to offer. * This variable is, for now, set to '10', which seems most reasonable. * * The reason right-aligned epochs don't use a "shortened version" (e.g. only showing the first letter) boils down to: * 1. The first letter of every time unit being unique is a property that might not carry across to other languages. * 2. Languages where displayed characters change depending on the characters preceding/following it will become unclear. * 3. Some polymorphic languages might not be able to appropriately convey meaning with one character (FRS is not a linguist- correct her on this if needed.) * * This right padlength is then incorporated into a so-called 'epoch_format' which, in turn, will be used to format the correct epoch. * If an external language introduces time units longer than 10 characters in size, consider altering these variables. * The game (likely) shan't segfault, though the text may appear a bit messed up if these variables aren't set properly. */ const int max_padlength = 10; /* Dealing With Screen Extremities * =============================== * 'maxlength' corresponds to the most extreme length a log message may be before foldstring() wraps it around to two or more lines. * The numbers subtracted from FULL_SCREEN_WIDTH are - in order: * '-2' the characters reserved for the borders of the box, both on the left and right side. * '-1' the leftmost guide character that's drawn on screen. * '-4' the padded three-digit number each epoch starts with. * '-max_padlength' the characters of space that are allocated to time units (e.g. "years", "minutes", etc.) * * 'bottom' works much like 'maxlength', but instead it refers to the amount of lines that the message box may hold. */ const int maxlength = FULL_SCREEN_WIDTH - 2 - 1 - 4 - max_padlength; const int bottom = FULL_SCREEN_HEIGHT - 2; const int msg_count = size(); /* Dealing With Scroll Direction * ============================= * Much like how the sidebar can have variable scroll direction, so will the message box. * To properly differentiate the two methods of displaying text, we will label them NEWEST-TOP, and OLDEST-TOP. This labeling should be self explanatory. * * Note that 'offset' tracks only our current position in the list; it shan't at all affect the manner in which the messages are drawn. * Messages are always drawn top-to-bottom. If NEWEST-TOP is used, then the top line (line=1) corresponds to the newest message. The one further down the second-newest, etc. * If the OLDEST-TOP method is used, then the top line (line=1) corresponds to the oldest message, and the bottom one to the newest. * The 'for (;;)' block below is nearly completely method-agnostic, save for the `player_messages.impl_->history(i)` call. * * In case of NEWEST-TOP, the 'i' variable easily enough corresponds to the newest message. * In case of OLDEST-TOP, the 'i' variable must be flipped- meaning the highest value of 'i' returns the result for the lowest value of 'i', etc. * To achieve this, the 'flip_message' variable is set to either the value of 'msg_count', or '0'. This variable is then subtracted from 'i' in each call to player_messages.impl_->history(); * * 'offset' refers to the corresponding message that will be displayed at the very TOP of the message box window. * NEWEST-TOP: 'offset' starts simply at '0' - the very top of the window. * OLDEST-TOP: 'offset' is set to the maximum value it could possibly be. That is- 'msg_count-bottom'. This way, the screen starts with the scrollbar all the way down. * 'retrieve_history' refers to the line that should be displayed- this is either 'i' if it's NEWEST-TOP, or a flipped version of 'i' if it's OLDEST-TOP. */ int offset = log_from_top ? 0 : ( msg_count - bottom ); const int flip = log_from_top ? 0 : msg_count - 1; for( ;; ) { werase( w ); draw_border( w ); mvwprintz( w, bottom + 1, 32, c_red, _( "Press %s to return" ), ctxt.get_desc( "QUIT" ).c_str() ); draw_scrollbar( w, offset, bottom, msg_count, 1, 0, c_white, true ); int line = 1; int lasttime = -1; for( int i = offset; i < msg_count; ++i ) { const int retrieve_history = abs( i - flip ); if( line > bottom ) { break; // This statement makes it so that no non-existent messages are printed (which usually results in a segfault) } else if( retrieve_history >= msg_count ) { continue; } const game_message &m = player_messages.impl_->history( retrieve_history ); const calendar timepassed = calendar::turn - m.timestamp_in_turns; std::string long_ago = to_string_clipped( time_duration::from_turns( timepassed ) ); nc_color col = msgtype_to_color( m.type, false ); // Here we separate the unit and amount from one another so that they can be properly padded when they're drawn on the screen. // Note that the very first character of 'unit' is often a space (except for languages where the time unit directly follows the number.) const auto amount_len = long_ago.find_first_not_of( "0123456789" ); std::string amount = long_ago.substr( 0, amount_len ); std::string unit = long_ago.substr( amount_len ); if( timepassed.get_turn() != lasttime ) { right_print( w, line, 2, c_light_blue, string_format( _( "%-3s%-10s" ), amount.c_str(), unit.c_str() ) ); lasttime = timepassed.get_turn(); } nc_color col_out = col; for( const std::string &folded : foldstring( m.get_with_count(), maxlength ) ) { if( line > bottom ) { break; } print_colored_text( w, line, 2, col_out, col, folded ); // So-called special "markers"- alternating '=' and '-'s at the edges of te message window so players can properly make sense of which message belongs to which time interval. // The '+offset%4' in the calculation makes it so that the markings scroll along with the messages. // On lines divisible by 4, draw a dark gray '-' at both horizontal extremes of the window. if( ( line + offset % 4 ) % 4 == 0 ) { mvwprintz( w, line, 1, c_dark_gray, "-" ); mvwprintz( w, line, FULL_SCREEN_WIDTH - 2, c_dark_gray, "-" ); // On lines divisible by 2 (but not 4), draw a light gray '=' at the horizontal extremes of the window. } else if( ( line + offset % 4 ) % 2 == 0 ) { mvwprintz( w, line, 1, c_light_gray, "=" ); mvwprintz( w, line, FULL_SCREEN_WIDTH - 2, c_light_gray, "=" ); } // Only now are we done with this line: line++; } } if( offset < msg_count - bottom ) { mvwprintz( w, bottom + 1, 5, c_magenta, "vvv" ); } if( offset > 0 ) { mvwprintz( w, bottom + 1, FULL_SCREEN_WIDTH - 8, c_magenta, "^^^" ); } wrefresh( w ); const std::string &action = ctxt.handle_input(); if( action == "DOWN" && offset < msg_count - bottom ) { offset++; } else if( action == "UP" && offset > 0 ) { offset--; } else if( action == "QUIT" ) { break; } } player_messages.impl_->curmes = calendar::turn.get_turn(); }