diff --git a/src/achievement.cpp b/src/achievement.cpp index 2de0dd9c20c2c..e2a12ccc22ca9 100644 --- a/src/achievement.cpp +++ b/src/achievement.cpp @@ -143,6 +143,7 @@ class requirement_watcher : stat_watcher public: requirement_watcher( achievement_tracker &tracker, const achievement_requirement &req, stats_tracker &stats ) : + current_value_( req.statistic->value( stats ) ), tracker_( &tracker ), requirement_( &req ) { stats.add_watcher( req.statistic, this ); @@ -153,60 +154,24 @@ class requirement_watcher : stat_watcher bool is_satisfied( stats_tracker &stats ) { return requirement_->satisifed_by( requirement_->statistic->value( stats ) ); } - private: - achievement_tracker *tracker_; - const achievement_requirement *requirement_; -}; - -class achievement_tracker -{ - public: - // Non-movable because requirement_watcher stores a pointer to us - achievement_tracker( const achievement_tracker & ) = delete; - achievement_tracker &operator=( const achievement_tracker & ) = delete; - - achievement_tracker( const achievement &a, achievements_tracker &tracker, - stats_tracker &stats ) : - achievement_( &a ), - tracker_( &tracker ) { - for( const achievement_requirement &req : a.requirements() ) { - watchers_.push_back( std::make_unique( *this, req, stats ) ); - } - - for( const std::unique_ptr &watcher : watchers_ ) { - bool is_satisfied = watcher->is_satisfied( stats ); - sorted_watchers_[is_satisfied].insert( watcher.get() ); - } - } - - void set_requirement( requirement_watcher *watcher, bool is_satisfied ) { - if( !sorted_watchers_[is_satisfied].insert( watcher ).second ) { - // No change - return; - } - - // Remove from other; check for completion. - sorted_watchers_[!is_satisfied].erase( watcher ); - assert( sorted_watchers_[0].size() + sorted_watchers_[1].size() == watchers_.size() ); - if( sorted_watchers_[false].empty() ) { - tracker_->report_achievement( achievement_, achievement_completion::completed ); - } + std::string ui_text() const { + bool is_satisfied = requirement_->satisifed_by( current_value_ ); + nc_color c = is_satisfied ? c_green : c_yellow; + int current = current_value_.get(); + std::string result = string_format( _( "%s/%s " ), current, requirement_->target ); + result += requirement_->statistic->description(); + return colorize( result, c ); } private: - const achievement *achievement_; - achievements_tracker *tracker_; - std::vector> watchers_; - - // sorted_watchers_ maintains two sets of watchers, categorised by - // whether they watch a satisfied or unsatisfied requirement. This - // allows us to check whether the achievment is met on each new stat - // value in O(1) time. - std::array, 2> sorted_watchers_; + cata_variant current_value_; + achievement_tracker *tracker_; + const achievement_requirement *requirement_; }; void requirement_watcher::new_value( const cata_variant &new_value, stats_tracker & ) { + current_value_ = new_value; tracker_->set_requirement( this, requirement_->satisifed_by( new_value ) ); } @@ -244,6 +209,61 @@ void achievement_state::deserialize( JsonIn &jsin ) jo.read( "last_state_change", last_state_change ); } +achievement_tracker::achievement_tracker( const achievement &a, achievements_tracker &tracker, + stats_tracker &stats ) : + achievement_( &a ), + tracker_( &tracker ) +{ + for( const achievement_requirement &req : a.requirements() ) { + watchers_.push_back( std::make_unique( *this, req, stats ) ); + } + + for( const std::unique_ptr &watcher : watchers_ ) { + bool is_satisfied = watcher->is_satisfied( stats ); + sorted_watchers_[is_satisfied].insert( watcher.get() ); + } +} + +void achievement_tracker::set_requirement( requirement_watcher *watcher, bool is_satisfied ) +{ + if( !sorted_watchers_[is_satisfied].insert( watcher ).second ) { + // No change + return; + } + + // Remove from other; check for completion. + sorted_watchers_[!is_satisfied].erase( watcher ); + assert( sorted_watchers_[0].size() + sorted_watchers_[1].size() == watchers_.size() ); + + if( sorted_watchers_[false].empty() ) { + tracker_->report_achievement( achievement_, achievement_completion::completed ); + } +} + +std::string achievement_tracker::ui_text( const achievement_state *state ) const +{ + auto color_from_completion = []( achievement_completion comp ) { + switch( comp ) { + case achievement_completion::pending: + return c_yellow; + case achievement_completion::completed: + return c_light_green; + case achievement_completion::last: + break; + } + debugmsg( "Invalid achievement_completion" ); + abort(); + }; + + achievement_completion comp = state ? state->completion : achievement_completion::pending; + nc_color c = color_from_completion( comp ); + std::string result = colorize( achievement_->description(), c ) + "\n"; + for( const std::unique_ptr &watcher : watchers_ ) { + result += " " + watcher->ui_text() + "\n"; + } + return result; +} + achievements_tracker::achievements_tracker( stats_tracker &stats, const std::function &achievement_attained_callback ) : @@ -287,6 +307,30 @@ void achievements_tracker::report_achievement( const achievement *a, achievement } } +achievement_completion achievements_tracker::is_completed( const string_id &id ) const +{ + auto it = achievements_status_.find( id ); + if( it == achievements_status_.end() ) { + return achievement_completion::pending; + } + return it->second.completion; +} + +std::string achievements_tracker::ui_text_for( const achievement *ach ) const +{ + auto state_it = achievements_status_.find( ach->id ); + const achievement_state *state = nullptr; + if( state_it != achievements_status_.end() ) { + state = &state_it->second; + } + auto watcher_it = watchers_.find( ach->id ); + if( watcher_it == watchers_.end() ) { + return colorize( ach->description() + _( "\nInternal error: achievement lacks watcher." ), + c_red ); + } + return watcher_it->second.ui_text( state ); +} + void achievements_tracker::clear() { watchers_.clear(); @@ -325,6 +369,8 @@ void achievements_tracker::deserialize( JsonIn &jsin ) void achievements_tracker::init_watchers() { for( const achievement *a : valid_achievements() ) { - watchers_.emplace_back( *a, *this, *stats_ ); + watchers_.emplace( + std::piecewise_construct, std::forward_as_tuple( a->id ), + std::forward_as_tuple( *a, *this, *stats_ ) ); } } diff --git a/src/achievement.h b/src/achievement.h index dfe2d9527c40d..8d14331934a29 100644 --- a/src/achievement.h +++ b/src/achievement.h @@ -14,7 +14,8 @@ class JsonObject; struct achievement_requirement; -class achievement_tracker; +class achievements_tracker; +class requirement_watcher; class stats_tracker; class achievement @@ -63,6 +64,31 @@ struct achievement_state { void deserialize( JsonIn & ); }; +class achievement_tracker +{ + public: + // Non-movable because requirement_watcher stores a pointer to us + achievement_tracker( const achievement_tracker & ) = delete; + achievement_tracker &operator=( const achievement_tracker & ) = delete; + + achievement_tracker( const achievement &a, achievements_tracker &tracker, + stats_tracker &stats ); + + void set_requirement( requirement_watcher *watcher, bool is_satisfied ); + + std::string ui_text( const achievement_state * ) const; + private: + const achievement *achievement_; + achievements_tracker *tracker_; + std::vector> watchers_; + + // sorted_watchers_ maintains two sets of watchers, categorised by + // whether they watch a satisfied or unsatisfied requirement. This + // allows us to check whether the achievment is met on each new stat + // value in O(1) time. + std::array, 2> sorted_watchers_; +}; + class achievements_tracker : public event_subscriber { public: @@ -79,6 +105,9 @@ class achievements_tracker : public event_subscriber void report_achievement( const achievement *, achievement_completion ); + achievement_completion is_completed( const string_id & ) const; + std::string ui_text_for( const achievement * ) const; + void clear(); void notify( const cata::event & ) override; @@ -89,7 +118,7 @@ class achievements_tracker : public event_subscriber stats_tracker *stats_ = nullptr; std::function achievement_attained_callback_; - std::list watchers_; + std::unordered_map, achievement_tracker> watchers_; std::unordered_set> initial_achievements_; std::unordered_map, achievement_state> achievements_status_; }; diff --git a/src/game.cpp b/src/game.cpp index 53001432a296a..a5ba3dab01e5d 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -2676,7 +2676,7 @@ void game::death_screen() { gamemode->game_over(); Messages::display_messages(); - show_scores_ui( stats(), get_kill_tracker() ); + show_scores_ui( *achievements_tracker_ptr, stats(), get_kill_tracker() ); disp_NPC_epilogues(); follower_ids.clear(); display_faction_epilogues(); diff --git a/src/handle_action.cpp b/src/handle_action.cpp index 4d7ad6620e182..30e4e12301723 100644 --- a/src/handle_action.cpp +++ b/src/handle_action.cpp @@ -2298,7 +2298,7 @@ bool game::handle_action() break; case ACTION_SCORES: - show_scores_ui( stats(), get_kill_tracker() ); + show_scores_ui( *achievements_tracker_ptr, stats(), get_kill_tracker() ); break; case ACTION_FACTIONS: diff --git a/src/scores_ui.cpp b/src/scores_ui.cpp index 12e973a679f61..b3840176f0166 100644 --- a/src/scores_ui.cpp +++ b/src/scores_ui.cpp @@ -5,6 +5,7 @@ #include #include +#include "achievement.h" #include "color.h" #include "cursesdef.h" #include "event_statistics.h" @@ -17,6 +18,31 @@ #include "ui.h" #include "ui_manager.h" +static std::string get_achievements_text( const achievements_tracker &achievements ) +{ + std::string os; + std::vector valid_achievements = achievements.valid_achievements(); + using sortable_achievement = + std::tuple; + std::vector sortable_achievements; + std::transform( valid_achievements.begin(), valid_achievements.end(), + std::back_inserter( sortable_achievements ), + [&]( const achievement * ach ) { + achievement_completion comp = achievements.is_completed( ach->id ); + return std::make_tuple( comp, ach->description().translated(), ach ); + } ); + std::sort( sortable_achievements.begin(), sortable_achievements.end() ); + for( const sortable_achievement &ach : sortable_achievements ) { + os += achievements.ui_text_for( std::get( ach ) ); + } + if( valid_achievements.empty() ) { + os += _( "This game has no valid achievements.\n" ); + } + os += _( "\nNote that only achievements that existed when you started this game and still " + "exist now will appear here." ); + return os; +} + static std::string get_scores_text( stats_tracker &stats ) { std::string os; @@ -32,21 +58,25 @@ static std::string get_scores_text( stats_tracker &stats ) return os; } -void show_scores_ui( stats_tracker &stats, const kill_tracker &kills ) +void show_scores_ui( const achievements_tracker &achievements, stats_tracker &stats, + const kill_tracker &kills ) { catacurses::window w = new_centered_win( FULL_SCREEN_HEIGHT, FULL_SCREEN_WIDTH ); enum class tab_mode { + achievements, scores, kills, num_tabs, - first_tab = scores, + first_tab = achievements, }; tab_mode tab = static_cast( 0 ); input_context ctxt( "SCORES" ); ctxt.register_cardinal(); ctxt.register_action( "QUIT" ); + ctxt.register_action( "PREV_TAB" ); + ctxt.register_action( "NEXT_TAB" ); ctxt.register_action( "HELP_KEYBINDINGS" ); catacurses::window w_view = catacurses::newwin( getmaxy( w ) - 4, getmaxx( w ) - 1, @@ -61,6 +91,7 @@ void show_scores_ui( stats_tracker &stats, const kill_tracker &kills ) werase( w ); const std::vector> tabs = { + { tab_mode::achievements, _( "ACHIEVEMENTS" ) }, { tab_mode::scores, _( "SCORES" ) }, { tab_mode::kills, _( "KILLS" ) }, }; @@ -69,6 +100,9 @@ void show_scores_ui( stats_tracker &stats, const kill_tracker &kills ) if( new_tab ) { switch( tab ) { + case tab_mode::achievements: + view.set_text( get_achievements_text( achievements ) ); + break; case tab_mode::scores: view.set_text( get_scores_text( stats ) ); break; @@ -87,13 +121,13 @@ void show_scores_ui( stats_tracker &stats, const kill_tracker &kills ) const std::string action = ctxt.handle_input(); new_tab = false; - if( action == "RIGHT" ) { + if( action == "RIGHT" || action == "NEXT_TAB" ) { tab = static_cast( static_cast( tab ) + 1 ); if( tab >= tab_mode::num_tabs ) { tab = tab_mode::first_tab; } new_tab = true; - } else if( action == "LEFT" ) { + } else if( action == "LEFT" || action == "PREV_TAB" ) { tab = static_cast( static_cast( tab ) - 1 ); if( tab < tab_mode::first_tab ) { tab = static_cast( static_cast( tab_mode::num_tabs ) - 1 ); @@ -103,9 +137,7 @@ void show_scores_ui( stats_tracker &stats, const kill_tracker &kills ) view.scroll_down(); } else if( action == "UP" ) { view.scroll_up(); - } else if( action == "CONFIRM" ) { - break; - } else if( action == "QUIT" ) { + } else if( action == "CONFIRM" || action == "QUIT" ) { break; } } diff --git a/src/scores_ui.h b/src/scores_ui.h index 7ff135241b86c..0b5ccc17782e1 100644 --- a/src/scores_ui.h +++ b/src/scores_ui.h @@ -1,9 +1,11 @@ #ifndef CATA_SCORES_UI_HPP #define CATA_SCORES_UI_HPP +class achievements_tracker; class kill_tracker; class stats_tracker; -void show_scores_ui( stats_tracker &, const kill_tracker & ); +void show_scores_ui( const achievements_tracker &achievements, stats_tracker &, + const kill_tracker & ); #endif // CATA_SCORES_UI_HPP diff --git a/tests/stats_tracker_test.cpp b/tests/stats_tracker_test.cpp index 1ef68dae64410..d2b17142dfafc 100644 --- a/tests/stats_tracker_test.cpp +++ b/tests/stats_tracker_test.cpp @@ -245,6 +245,9 @@ TEST_CASE( "achievments_tracker", "[stats]" ) b.send( avatar_zombie_kill ); REQUIRE( achievement_completed != nullptr ); CHECK( achievement_completed->id.str() == "achievement_kill_zombie" ); + CHECK( a.ui_text_for( achievement_completed ) == + "One down, billions to go…\n" + " 1/1 Number of zombies killed\n" ); } }