Skip to content

Commit

Permalink
Add achievements tab to score window (#39540)
Browse files Browse the repository at this point in the history
* Support PREV_TAB, NEXT_TAB in scores window

* Add achievements tab to scores window

Want the player to be able to see the achievements they are working
towards.

* Add achievements text unit test
  • Loading branch information
jbytheway authored Apr 14, 2020
1 parent 922d2fd commit 06514d0
Show file tree
Hide file tree
Showing 7 changed files with 172 additions and 60 deletions.
142 changes: 94 additions & 48 deletions src/achievement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
Expand All @@ -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<requirement_watcher>( *this, req, stats ) );
}

for( const std::unique_ptr<requirement_watcher> &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<int>();
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<std::unique_ptr<requirement_watcher>> 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<std::unordered_set<requirement_watcher *>, 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 ) );
}

Expand Down Expand Up @@ -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<requirement_watcher>( *this, req, stats ) );
}

for( const std::unique_ptr<requirement_watcher> &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<requirement_watcher> &watcher : watchers_ ) {
result += " " + watcher->ui_text() + "\n";
}
return result;
}

achievements_tracker::achievements_tracker(
stats_tracker &stats,
const std::function<void( const achievement * )> &achievement_attained_callback ) :
Expand Down Expand Up @@ -287,6 +307,30 @@ void achievements_tracker::report_achievement( const achievement *a, achievement
}
}

achievement_completion achievements_tracker::is_completed( const string_id<achievement> &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();
Expand Down Expand Up @@ -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_ ) );
}
}
33 changes: 31 additions & 2 deletions src/achievement.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@

class JsonObject;
struct achievement_requirement;
class achievement_tracker;
class achievements_tracker;
class requirement_watcher;
class stats_tracker;

class achievement
Expand Down Expand Up @@ -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<std::unique_ptr<requirement_watcher>> 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<std::unordered_set<requirement_watcher *>, 2> sorted_watchers_;
};

class achievements_tracker : public event_subscriber
{
public:
Expand All @@ -79,6 +105,9 @@ class achievements_tracker : public event_subscriber

void report_achievement( const achievement *, achievement_completion );

achievement_completion is_completed( const string_id<achievement> & ) const;
std::string ui_text_for( const achievement * ) const;

void clear();
void notify( const cata::event & ) override;

Expand All @@ -89,7 +118,7 @@ class achievements_tracker : public event_subscriber

stats_tracker *stats_ = nullptr;
std::function<void( const achievement * )> achievement_attained_callback_;
std::list<achievement_tracker> watchers_;
std::unordered_map<string_id<achievement>, achievement_tracker> watchers_;
std::unordered_set<string_id<achievement>> initial_achievements_;
std::unordered_map<string_id<achievement>, achievement_state> achievements_status_;
};
Expand Down
2 changes: 1 addition & 1 deletion src/game.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion src/handle_action.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
46 changes: 39 additions & 7 deletions src/scores_ui.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include <utility>
#include <vector>

#include "achievement.h"
#include "color.h"
#include "cursesdef.h"
#include "event_statistics.h"
Expand All @@ -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<const achievement *> valid_achievements = achievements.valid_achievements();
using sortable_achievement =
std::tuple<achievement_completion, std::string, const achievement *>;
std::vector<sortable_achievement> 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<const achievement *>( 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;
Expand All @@ -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<tab_mode>( 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,
Expand All @@ -61,6 +91,7 @@ void show_scores_ui( stats_tracker &stats, const kill_tracker &kills )
werase( w );

const std::vector<std::pair<tab_mode, std::string>> tabs = {
{ tab_mode::achievements, _( "ACHIEVEMENTS" ) },
{ tab_mode::scores, _( "SCORES" ) },
{ tab_mode::kills, _( "KILLS" ) },
};
Expand All @@ -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;
Expand All @@ -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<tab_mode>( static_cast<int>( 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<tab_mode>( static_cast<int>( tab ) - 1 );
if( tab < tab_mode::first_tab ) {
tab = static_cast<tab_mode>( static_cast<int>( tab_mode::num_tabs ) - 1 );
Expand All @@ -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;
}
}
Expand Down
4 changes: 3 additions & 1 deletion src/scores_ui.h
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions tests/stats_tracker_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 ) ==
"<color_c_light_green>One down, billions to go…</color>\n"
" <color_c_green>1/1 Number of zombies killed</color>\n" );
}
}

Expand Down

0 comments on commit 06514d0

Please sign in to comment.