From 82b6ecba65e093f8477810936fef450c7e39f7de Mon Sep 17 00:00:00 2001 From: cakepie Date: Wed, 22 Mar 2023 09:57:09 +0800 Subject: [PATCH] Update events to account for ability to swap avatars (debug or upon death) (#63488) --- data/json/statistics.json | 11 +- src/avatar.cpp | 10 +- src/avatar.h | 4 +- src/debug_menu.cpp | 2 +- src/do_turn.cpp | 193 +-------------------------------- src/event.cpp | 6 +- src/event.h | 41 +++++-- src/event_statistics.cpp | 2 +- src/game.cpp | 200 ++++++++++++++++++++++++++++++++++- src/game.h | 1 + src/memorial_logger.cpp | 22 ++-- src/past_games_info.cpp | 39 ++++--- src/savegame_json.cpp | 38 +++++++ tests/memorial_test.cpp | 12 +-- tests/stats_tracker_test.cpp | 14 ++- 15 files changed, 355 insertions(+), 240 deletions(-) diff --git a/data/json/statistics.json b/data/json/statistics.json index 158e7f932945e..55f8e5c623397 100644 --- a/data/json/statistics.json +++ b/data/json/statistics.json @@ -2,10 +2,17 @@ { "id": "avatar_id", "type": "event_statistic", - "stat_type": "unique_value", - "event_type": "game_start", + "stat_type": "last_value", + "event_type": "game_avatar_new", "field": "avatar_id" }, + { + "id": "last_words", + "type": "event_statistic", + "stat_type": "last_value", + "event_type": "game_avatar_death", + "field": "last_words" + }, { "id": "avatar_wakes_up", "type": "event_transformation", diff --git a/src/avatar.cpp b/src/avatar.cpp index 4552aec463227..3b8d3ff79a755 100644 --- a/src/avatar.cpp +++ b/src/avatar.cpp @@ -166,7 +166,7 @@ static void swap_npc( npc &one, npc &two, npc &tmp ) two = std::move( tmp ); } -void avatar::control_npc( npc &np ) +void avatar::control_npc( npc &np, const bool debug ) { if( !np.is_player_ally() ) { debugmsg( "control_npc() called on non-allied npc %s", np.name ); @@ -202,9 +202,13 @@ void avatar::control_npc( npc &np ) const bool z_level_changed = g->vertical_shift( posz() ); g->update_map( *this, z_level_changed ); character_mood_face( true ); + + profession_id prof_id = prof ? prof->ident() : profession::generic()->ident(); + get_event_bus().send( /*is_new_game=*/false, debug, + getID(), name, male, prof_id, custom_profession ); } -void avatar::control_npc_menu() +void avatar::control_npc_menu( const bool debug ) { std::vector> followers; uilist charmenu; @@ -225,7 +229,7 @@ void avatar::control_npc_menu() if( charmenu.ret < 0 || static_cast( charmenu.ret ) >= followers.size() ) { return; } - get_avatar().control_npc( *followers[charmenu.ret] ); + get_avatar().control_npc( *followers[charmenu.ret], debug ); } void avatar::longpull( const std::string &name ) diff --git a/src/avatar.h b/src/avatar.h index 714cf23ca0d17..aa6a5ac286d33 100644 --- a/src/avatar.h +++ b/src/avatar.h @@ -126,11 +126,11 @@ class avatar : public Character * Makes the avatar "take over" the given NPC, while the current avatar character * becomes an NPC. */ - void control_npc( npc & ); + void control_npc( npc &, bool debug = false ); /** * Open a menu to choose the NPC to take over. */ - void control_npc_menu(); + void control_npc_menu( bool debug = false ); using Character::query_yn; bool query_yn( const std::string &mes ) const override; diff --git a/src/debug_menu.cpp b/src/debug_menu.cpp index 2838aa1e0ebc0..4ef8063d0ed1c 100644 --- a/src/debug_menu.cpp +++ b/src/debug_menu.cpp @@ -1377,7 +1377,7 @@ static void spawn_nested_mapgen() static void control_npc_menu() { - get_avatar().control_npc_menu(); + get_avatar().control_npc_menu( true ); } static void character_edit_stats_menu( Character &you ) diff --git a/src/do_turn.cpp b/src/do_turn.cpp index 3795feb28220a..a34a3abb0bcda 100644 --- a/src/do_turn.cpp +++ b/src/do_turn.cpp @@ -29,6 +29,7 @@ #include "scent_map.h" #include "sdlsound.h" #include "string_input_popup.h" +#include "stats_tracker.h" #include "timed_event.h" #include "ui_manager.h" #include "vehicle.h" @@ -47,13 +48,9 @@ static const efftype_id effect_npc_suspend( "npc_suspend" ); static const efftype_id effect_ridden( "ridden" ); static const efftype_id effect_sleep( "sleep" ); -static const itype_id itype_holybook_bible1( "holybook_bible1" ); -static const itype_id itype_holybook_bible2( "holybook_bible2" ); -static const itype_id itype_holybook_bible3( "holybook_bible3" ); +static const event_statistic_id event_statistic_last_words( "last_words" ); -static const trait_id trait_CANNIBAL( "CANNIBAL" ); static const trait_id trait_HAS_NEMESIS( "HAS_NEMESIS" ); -static const trait_id trait_PSYCHOPATH( "PSYCHOPATH" ); #if defined(__ANDROID__) extern std::map> quick_shortcuts_map; @@ -85,197 +82,17 @@ bool cleanup_at_end() // and the overmap, and the local map. g->save_maps(); //Omap also contains the npcs who need to be saved. - } - - if( g->uquit == QUIT_DIED || g->uquit == QUIT_SUICIDE ) { - std::vector vRip; - - int iMaxWidth = 0; - int iNameLine = 0; - int iInfoLine = 0; - - if( u.has_amount( itype_holybook_bible1, 1 ) || u.has_amount( itype_holybook_bible2, 1 ) || - u.has_amount( itype_holybook_bible3, 1 ) ) { - if( !( u.has_trait( trait_CANNIBAL ) || u.has_trait( trait_PSYCHOPATH ) ) ) { - vRip.emplace_back( " _______ ___" ); - vRip.emplace_back( " < `/ |" ); - vRip.emplace_back( " > _ _ (" ); - vRip.emplace_back( " | |_) | |_) |" ); - vRip.emplace_back( " | | \\ | | |" ); - vRip.emplace_back( " ______.__%_| |_________ __" ); - vRip.emplace_back( " _/ \\| |" ); - iNameLine = vRip.size(); - vRip.emplace_back( "| <" ); - vRip.emplace_back( "| |" ); - iMaxWidth = utf8_width( vRip.back() ); - vRip.emplace_back( "| |" ); - vRip.emplace_back( "|_____.-._____ __/|_________|" ); - vRip.emplace_back( " | |" ); - iInfoLine = vRip.size(); - vRip.emplace_back( " | |" ); - vRip.emplace_back( " | <" ); - vRip.emplace_back( " | |" ); - vRip.emplace_back( " | _ |" ); - vRip.emplace_back( " |__/ |" ); - vRip.emplace_back( " % / `--. |%" ); - vRip.emplace_back( " * .%%| -< @%%%" ); // NOLINT(cata-text-style) - vRip.emplace_back( " `\\%`@| |@@%@%%" ); - vRip.emplace_back( " .%%%@@@|% ` % @@@%%@%%%%" ); - vRip.emplace_back( " _.%%%%%%@@@@@@%%%__/\\%@@%%@@@@@@@%%%%%%" ); - - } else { - vRip.emplace_back( " _______ ___" ); - vRip.emplace_back( " | \\/ |" ); - vRip.emplace_back( " | |" ); - vRip.emplace_back( " | |" ); - iInfoLine = vRip.size(); - vRip.emplace_back( " | |" ); - vRip.emplace_back( " | |" ); - vRip.emplace_back( " | |" ); - vRip.emplace_back( " | |" ); - vRip.emplace_back( " | <" ); - vRip.emplace_back( " | _ |" ); - vRip.emplace_back( " |__/ |" ); - vRip.emplace_back( " ______.__%_| |__________ _" ); - vRip.emplace_back( " _/ \\| \\" ); - iNameLine = vRip.size(); - vRip.emplace_back( "| <" ); - vRip.emplace_back( "| |" ); - iMaxWidth = utf8_width( vRip.back() ); - vRip.emplace_back( "| |" ); - vRip.emplace_back( "|_____.-._______ __/|__________|" ); - vRip.emplace_back( " % / `_-. _ |%" ); - vRip.emplace_back( " * .%%| |_) | |_)< @%%%" ); // NOLINT(cata-text-style) - vRip.emplace_back( " `\\%`@| | \\ | | |@@%@%%" ); - vRip.emplace_back( " .%%%@@@|% ` % @@@%%@%%%%" ); - vRip.emplace_back( " _.%%%%%%@@@@@@%%%__/\\%@@%%@@@@@@@%%%%%%" ); - } - } else { - vRip.emplace_back( R"( _________ ____ )" ); - vRip.emplace_back( R"( _/ `/ \_ )" ); - vRip.emplace_back( R"( _/ _ _ \_. )" ); - vRip.emplace_back( R"( _%\ |_) | |_) \_ )" ); - vRip.emplace_back( R"( _/ \/ | \ | | \_ )" ); - vRip.emplace_back( R"( _/ \_ )" ); - vRip.emplace_back( R"(| |)" ); - iNameLine = vRip.size(); - vRip.emplace_back( R"( ) < )" ); - vRip.emplace_back( R"(| |)" ); - vRip.emplace_back( R"(| |)" ); - vRip.emplace_back( R"(| _ |)" ); - vRip.emplace_back( R"(|__/ |)" ); - iMaxWidth = utf8_width( vRip.back() ); - vRip.emplace_back( R"( / `--. |)" ); - vRip.emplace_back( R"(| ( )" ); - iInfoLine = vRip.size(); - vRip.emplace_back( R"(| |)" ); - vRip.emplace_back( R"(| |)" ); - vRip.emplace_back( R"(| % . |)" ); - vRip.emplace_back( R"(| @` %% |)" ); - vRip.emplace_back( R"(| %@%@%\ * %`%@%|)" ); - vRip.emplace_back( R"(%%@@@.%@%\%% `\ %%.%%@@%@)" ); - vRip.emplace_back( R"(@%@@%%%%%@@@@@@%%%%%%%%@@%%@@@%%%@%%@)" ); - } - - const point iOffset( TERMX > FULL_SCREEN_WIDTH ? ( TERMX - FULL_SCREEN_WIDTH ) / 2 : 0, - TERMY > FULL_SCREEN_HEIGHT ? ( TERMY - FULL_SCREEN_HEIGHT ) / 2 : 0 ); - - catacurses::window w_rip = catacurses::newwin( FULL_SCREEN_HEIGHT, FULL_SCREEN_WIDTH, - iOffset ); - draw_border( w_rip ); - - sfx::do_player_death_hurt( get_player_character(), true ); - sfx::fade_audio_group( sfx::group::weather, 2000 ); - sfx::fade_audio_group( sfx::group::time_of_day, 2000 ); - sfx::fade_audio_group( sfx::group::context_themes, 2000 ); - sfx::fade_audio_group( sfx::group::fatigue, 2000 ); - - for( size_t iY = 0; iY < vRip.size(); ++iY ) { - size_t iX = 0; - const char *str = vRip[iY].data(); - for( int slen = vRip[iY].size(); slen > 0; ) { - const uint32_t cTemp = UTF8_getch( &str, &slen ); - if( cTemp != U' ' ) { - nc_color ncColor = c_light_gray; - - if( cTemp == U'%' ) { - ncColor = c_green; - - } else if( cTemp == U'_' || cTemp == U'|' ) { - ncColor = c_white; - - } else if( cTemp == U'@' ) { - ncColor = c_brown; - - } else if( cTemp == U'*' ) { - ncColor = c_red; - } - - mvwputch( w_rip, point( iX + FULL_SCREEN_WIDTH / 2 - ( iMaxWidth / 2 ), iY + 1 ), ncColor, - cTemp ); - } - iX += mk_wcwidth( cTemp ); - } - } - - std::string sTemp; - - center_print( w_rip, iInfoLine++, c_white, _( "Survived:" ) ); - - const time_duration survived = calendar::turn - calendar::start_of_game; - const int minutes = to_minutes( survived ) % 60; - const int hours = to_hours( survived ) % 24; - const int days = to_days( survived ); - - if( days > 0 ) { - // NOLINTNEXTLINE(cata-translate-string-literal) - sTemp = string_format( "%dd %dh %dm", days, hours, minutes ); - } else if( hours > 0 ) { - // NOLINTNEXTLINE(cata-translate-string-literal) - sTemp = string_format( "%dh %dm", hours, minutes ); - } else { - // NOLINTNEXTLINE(cata-translate-string-literal) - sTemp = string_format( "%dm", minutes ); - } - - center_print( w_rip, iInfoLine++, c_white, sTemp ); - - const int iTotalKills = g->get_kill_tracker().monster_kill_count(); - - sTemp = _( "Kills:" ); - mvwprintz( w_rip, point( FULL_SCREEN_WIDTH / 2 - 5, 1 + iInfoLine++ ), c_light_gray, - ( sTemp + " " ) ); - wprintz( w_rip, c_magenta, "%d", iTotalKills ); - - sTemp = _( "In memory of:" ); - mvwprintz( w_rip, point( FULL_SCREEN_WIDTH / 2 - utf8_width( sTemp ) / 2, iNameLine++ ), - c_light_gray, - sTemp ); - - sTemp = u.get_name(); - mvwprintz( w_rip, point( FULL_SCREEN_WIDTH / 2 - utf8_width( sTemp ) / 2, iNameLine++ ), c_white, - sTemp ); - - sTemp = _( "Last Words:" ); - mvwprintz( w_rip, point( FULL_SCREEN_WIDTH / 2 - utf8_width( sTemp ) / 2, iNameLine++ ), - c_light_gray, - sTemp ); - int iStartX = FULL_SCREEN_WIDTH / 2 - ( ( iMaxWidth - 4 ) / 2 ); - std::string sLastWords = string_input_popup() - .window( w_rip, point( iStartX, iNameLine ), iStartX + iMaxWidth - 4 - 1 ) - .max_length( iMaxWidth - 4 - 1 ) - .query_string(); g->death_screen(); - const bool is_suicide = g->uquit == QUIT_SUICIDE; std::chrono::seconds time_since_load = std::chrono::duration_cast( std::chrono::steady_clock::now() - g->time_of_last_load ); std::chrono::seconds total_time_played = g->time_played_at_last_load + time_since_load; - get_event_bus().send( is_suicide, sLastWords, total_time_played ); + get_event_bus().send( total_time_played ); // Struck the save_player_data here to forestall Weirdness g->move_save_to_graveyard(); - g->write_memorial_file( sLastWords ); + g->write_memorial_file( g->stats().value_of( event_statistic_last_words ) + .get() ); get_memorial().clear(); std::vector characters = g->list_active_saves(); // remove current player from the active characters list, as they are dead diff --git a/src/event.cpp b/src/event.cpp index a0075345da11b..2ddf279d2bbe6 100644 --- a/src/event.cpp +++ b/src/event.cpp @@ -74,6 +74,8 @@ std::string enum_to_string( event_type data ) case event_type::gains_addiction: return "gains_addiction"; case event_type::gains_mutation: return "gains_mutation"; case event_type::gains_skill_level: return "gains_skill_level"; + case event_type::game_avatar_death: return "game_avatar_death"; + case event_type::game_avatar_new: return "game_avatar_new"; case event_type::game_load: return "game_load"; case event_type::game_over: return "game_over"; case event_type::game_save: return "game_save"; @@ -124,7 +126,7 @@ DEFINE_EVENT_HELPER_FIELDS( event_spec_empty ) DEFINE_EVENT_HELPER_FIELDS( event_spec_character ) DEFINE_EVENT_HELPER_FIELDS( event_spec_character_item ) -static_assert( static_cast( event_type::num_event_types ) == 87, +static_assert( static_cast( event_type::num_event_types ) == 89, "This static_assert is a reminder to add a definition below when you add a new " "event_type. If your event_spec specialization inherits from another struct for " "its fields definition then you probably don't need a definition here." ); @@ -165,6 +167,8 @@ DEFINE_EVENT_FIELDS( fuel_tank_explodes ) DEFINE_EVENT_FIELDS( gains_addiction ) DEFINE_EVENT_FIELDS( gains_mutation ) DEFINE_EVENT_FIELDS( gains_skill_level ) +DEFINE_EVENT_FIELDS( game_avatar_death ) +DEFINE_EVENT_FIELDS( game_avatar_new ) DEFINE_EVENT_FIELDS( game_load ) DEFINE_EVENT_FIELDS( game_over ) DEFINE_EVENT_FIELDS( game_save ) diff --git a/src/event.h b/src/event.h index 75716f2a01360..edcb413401716 100644 --- a/src/event.h +++ b/src/event.h @@ -85,6 +85,8 @@ enum class event_type : int { gains_addiction, gains_mutation, gains_skill_level, + game_avatar_death, + game_avatar_new, game_load, game_over, game_save, @@ -172,7 +174,7 @@ struct event_spec_character_item { }; }; -static_assert( static_cast( event_type::num_event_types ) == 87, +static_assert( static_cast( event_type::num_event_types ) == 89, "This static_assert is to remind you to add a specialization for your new " "event_type below" ); @@ -556,6 +558,32 @@ struct event_spec { }; }; +template<> +struct event_spec { + static constexpr std::array, 5> fields = {{ + { "avatar_id", cata_variant_type::character_id }, + { "avatar_name", cata_variant_type::string }, + { "avatar_is_male", cata_variant_type::bool_ }, + { "is_suicide", cata_variant_type::bool_ }, + { "last_words", cata_variant_type::string }, + } + }; +}; + +template<> +struct event_spec { + static constexpr std::array, 7> fields = {{ + { "is_new_game", cata_variant_type::bool_ }, + { "is_debug", cata_variant_type::bool_ }, + { "avatar_id", cata_variant_type::character_id }, + { "avatar_name", cata_variant_type::string }, + { "avatar_is_male", cata_variant_type::bool_ }, + { "avatar_profession", cata_variant_type::profession_id }, + { "avatar_custom_profession", cata_variant_type::string }, + } + }; +}; + template<> struct event_spec { static constexpr std::array, 1> fields = {{ @@ -566,9 +594,7 @@ struct event_spec { template<> struct event_spec { - static constexpr std::array, 3> fields = {{ - { "is_suicide", cata_variant_type::bool_ }, - { "last_words", cata_variant_type::string }, + static constexpr std::array, 1> fields = {{ { "total_time_played", cata_variant_type::chrono_seconds }, } }; @@ -585,12 +611,7 @@ struct event_spec { template<> struct event_spec { - static constexpr std::array, 6> fields = {{ - { "avatar_id", cata_variant_type::character_id }, - { "avatar_name", cata_variant_type::string }, - { "avatar_is_male", cata_variant_type::bool_ }, - { "avatar_profession", cata_variant_type::profession_id }, - { "avatar_custom_profession", cata_variant_type::string }, + static constexpr std::array, 1> fields = {{ { "game_version", cata_variant_type::string }, } }; diff --git a/src/event_statistics.cpp b/src/event_statistics.cpp index a1a6a32fd8011..7d68ae2c2455d 100644 --- a/src/event_statistics.cpp +++ b/src/event_statistics.cpp @@ -724,7 +724,7 @@ struct event_statistic_count : event_statistic::impl { } monotonically monotonicity() const override { - return source->monotonicity(); + return monotonically::increasing; } std::unique_ptr clone() const override { diff --git a/src/game.cpp b/src/game.cpp index ec21d3e8afc56..c33c57d8e4edc 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -250,6 +250,9 @@ static const itype_id fuel_type_animal( "animal" ); static const itype_id itype_battery( "battery" ); static const itype_id itype_disassembly( "disassembly" ); static const itype_id itype_grapnel( "grapnel" ); +static const itype_id itype_holybook_bible1( "holybook_bible1" ); +static const itype_id itype_holybook_bible2( "holybook_bible2" ); +static const itype_id itype_holybook_bible3( "holybook_bible3" ); static const itype_id itype_manhole_cover( "manhole_cover" ); static const itype_id itype_remotevehcontrol( "remotevehcontrol" ); static const itype_id itype_rope_30( "rope_30" ); @@ -291,6 +294,7 @@ static const species_id species_PLANT( "PLANT" ); static const string_id npc_template_cyborg_rescued( "cyborg_rescued" ); static const trait_id trait_BADKNEES( "BADKNEES" ); +static const trait_id trait_CANNIBAL( "CANNIBAL" ); static const trait_id trait_CENOBITE( "CENOBITE" ); static const trait_id trait_ILLITERATE( "ILLITERATE" ); static const trait_id trait_INATTENTIVE( "INATTENTIVE" ); @@ -303,6 +307,7 @@ static const trait_id trait_M_IMMUNE( "M_IMMUNE" ); static const trait_id trait_NPC_STARTING_NPC( "NPC_STARTING_NPC" ); static const trait_id trait_NPC_STATIC_NPC( "NPC_STATIC_NPC" ); static const trait_id trait_PROF_CHURL( "PROF_CHURL" ); +static const trait_id trait_PSYCHOPATH( "PSYCHOPATH" ); static const trait_id trait_THICKSKIN( "THICKSKIN" ); static const trait_id trait_VINES2( "VINES2" ); static const trait_id trait_VINES3( "VINES3" ); @@ -1004,8 +1009,9 @@ bool game::start_game() } } - get_event_bus().send( u.getID(), u.name, u.male, u.prof->ident(), - u.custom_profession, getVersionString() ); + get_event_bus().send( getVersionString() ); + get_event_bus().send( /*is_new_game=*/true, /*is_debug=*/false, + u.getID(), u.name, u.male, u.prof->ident(), u.custom_profession ); time_played_at_last_load = std::chrono::seconds( 0 ); time_of_last_load = std::chrono::steady_clock::now(); tripoint_abs_omt abs_omt = u.global_omt_location(); @@ -2610,6 +2616,7 @@ bool game::is_game_over() return true; } if( uquit == QUIT_SUICIDE ) { + bury_screen(); if( u.in_vehicle ) { m.unboard_vehicle( u.pos() ); } @@ -2624,6 +2631,7 @@ bool game::is_game_over() if( !u.is_dead_state() ) { return false; } + bury_screen(); effect_on_conditions::avatar_death(); if( !u.is_dead_state() ) { return false; @@ -2646,6 +2654,194 @@ bool game::is_game_over() return false; } +void game::bury_screen() const +{ + avatar &u = get_avatar(); + + std::vector vRip; + + int iMaxWidth = 0; + int iNameLine = 0; + int iInfoLine = 0; + + if( u.has_amount( itype_holybook_bible1, 1 ) || u.has_amount( itype_holybook_bible2, 1 ) || + u.has_amount( itype_holybook_bible3, 1 ) ) { + if( !( u.has_trait( trait_CANNIBAL ) || u.has_trait( trait_PSYCHOPATH ) ) ) { + vRip.emplace_back( " _______ ___" ); + vRip.emplace_back( " < `/ |" ); + vRip.emplace_back( " > _ _ (" ); + vRip.emplace_back( " | |_) | |_) |" ); + vRip.emplace_back( " | | \\ | | |" ); + vRip.emplace_back( " ______.__%_| |_________ __" ); + vRip.emplace_back( " _/ \\| |" ); + iNameLine = vRip.size(); + vRip.emplace_back( "| <" ); + vRip.emplace_back( "| |" ); + iMaxWidth = utf8_width( vRip.back() ); + vRip.emplace_back( "| |" ); + vRip.emplace_back( "|_____.-._____ __/|_________|" ); + vRip.emplace_back( " | |" ); + iInfoLine = vRip.size(); + vRip.emplace_back( " | |" ); + vRip.emplace_back( " | <" ); + vRip.emplace_back( " | |" ); + vRip.emplace_back( " | _ |" ); + vRip.emplace_back( " |__/ |" ); + vRip.emplace_back( " % / `--. |%" ); + vRip.emplace_back( " * .%%| -< @%%%" ); // NOLINT(cata-text-style) + vRip.emplace_back( " `\\%`@| |@@%@%%" ); + vRip.emplace_back( " .%%%@@@|% ` % @@@%%@%%%%" ); + vRip.emplace_back( " _.%%%%%%@@@@@@%%%__/\\%@@%%@@@@@@@%%%%%%" ); + + } else { + vRip.emplace_back( " _______ ___" ); + vRip.emplace_back( " | \\/ |" ); + vRip.emplace_back( " | |" ); + vRip.emplace_back( " | |" ); + iInfoLine = vRip.size(); + vRip.emplace_back( " | |" ); + vRip.emplace_back( " | |" ); + vRip.emplace_back( " | |" ); + vRip.emplace_back( " | |" ); + vRip.emplace_back( " | <" ); + vRip.emplace_back( " | _ |" ); + vRip.emplace_back( " |__/ |" ); + vRip.emplace_back( " ______.__%_| |__________ _" ); + vRip.emplace_back( " _/ \\| \\" ); + iNameLine = vRip.size(); + vRip.emplace_back( "| <" ); + vRip.emplace_back( "| |" ); + iMaxWidth = utf8_width( vRip.back() ); + vRip.emplace_back( "| |" ); + vRip.emplace_back( "|_____.-._______ __/|__________|" ); + vRip.emplace_back( " % / `_-. _ |%" ); + vRip.emplace_back( " * .%%| |_) | |_)< @%%%" ); // NOLINT(cata-text-style) + vRip.emplace_back( " `\\%`@| | \\ | | |@@%@%%" ); + vRip.emplace_back( " .%%%@@@|% ` % @@@%%@%%%%" ); + vRip.emplace_back( " _.%%%%%%@@@@@@%%%__/\\%@@%%@@@@@@@%%%%%%" ); + } + } else { + vRip.emplace_back( R"( _________ ____ )" ); + vRip.emplace_back( R"( _/ `/ \_ )" ); + vRip.emplace_back( R"( _/ _ _ \_. )" ); + vRip.emplace_back( R"( _%\ |_) | |_) \_ )" ); + vRip.emplace_back( R"( _/ \/ | \ | | \_ )" ); + vRip.emplace_back( R"( _/ \_ )" ); + vRip.emplace_back( R"(| |)" ); + iNameLine = vRip.size(); + vRip.emplace_back( R"( ) < )" ); + vRip.emplace_back( R"(| |)" ); + vRip.emplace_back( R"(| |)" ); + vRip.emplace_back( R"(| _ |)" ); + vRip.emplace_back( R"(|__/ |)" ); + iMaxWidth = utf8_width( vRip.back() ); + vRip.emplace_back( R"( / `--. |)" ); + vRip.emplace_back( R"(| ( )" ); + iInfoLine = vRip.size(); + vRip.emplace_back( R"(| |)" ); + vRip.emplace_back( R"(| |)" ); + vRip.emplace_back( R"(| % . |)" ); + vRip.emplace_back( R"(| @` %% |)" ); + vRip.emplace_back( R"(| %@%@%\ * %`%@%|)" ); + vRip.emplace_back( R"(%%@@@.%@%\%% `\ %%.%%@@%@)" ); + vRip.emplace_back( R"(@%@@%%%%%@@@@@@%%%%%%%%@@%%@@@%%%@%%@)" ); + } + + const point iOffset( TERMX > FULL_SCREEN_WIDTH ? ( TERMX - FULL_SCREEN_WIDTH ) / 2 : 0, + TERMY > FULL_SCREEN_HEIGHT ? ( TERMY - FULL_SCREEN_HEIGHT ) / 2 : 0 ); + + catacurses::window w_rip = catacurses::newwin( FULL_SCREEN_HEIGHT, FULL_SCREEN_WIDTH, + iOffset ); + draw_border( w_rip ); + + sfx::do_player_death_hurt( get_player_character(), true ); + sfx::fade_audio_group( sfx::group::weather, 2000 ); + sfx::fade_audio_group( sfx::group::time_of_day, 2000 ); + sfx::fade_audio_group( sfx::group::context_themes, 2000 ); + sfx::fade_audio_group( sfx::group::fatigue, 2000 ); + + for( size_t iY = 0; iY < vRip.size(); ++iY ) { + size_t iX = 0; + const char *str = vRip[iY].data(); + for( int slen = vRip[iY].size(); slen > 0; ) { + const uint32_t cTemp = UTF8_getch( &str, &slen ); + if( cTemp != U' ' ) { + nc_color ncColor = c_light_gray; + + if( cTemp == U'%' ) { + ncColor = c_green; + + } else if( cTemp == U'_' || cTemp == U'|' ) { + ncColor = c_white; + + } else if( cTemp == U'@' ) { + ncColor = c_brown; + + } else if( cTemp == U'*' ) { + ncColor = c_red; + } + + mvwputch( w_rip, point( iX + FULL_SCREEN_WIDTH / 2 - ( iMaxWidth / 2 ), iY + 1 ), ncColor, + cTemp ); + } + iX += mk_wcwidth( cTemp ); + } + } + + std::string sTemp; + + center_print( w_rip, iInfoLine++, c_white, _( "Survived:" ) ); + + const time_duration survived = calendar::turn - calendar::start_of_game; + const int minutes = to_minutes( survived ) % 60; + const int hours = to_hours( survived ) % 24; + const int days = to_days( survived ); + + if( days > 0 ) { + // NOLINTNEXTLINE(cata-translate-string-literal) + sTemp = string_format( "%dd %dh %dm", days, hours, minutes ); + } else if( hours > 0 ) { + // NOLINTNEXTLINE(cata-translate-string-literal) + sTemp = string_format( "%dh %dm", hours, minutes ); + } else { + // NOLINTNEXTLINE(cata-translate-string-literal) + sTemp = string_format( "%dm", minutes ); + } + + center_print( w_rip, iInfoLine++, c_white, sTemp ); + + const int iTotalKills = g->get_kill_tracker().monster_kill_count(); + + sTemp = _( "Kills:" ); + mvwprintz( w_rip, point( FULL_SCREEN_WIDTH / 2 - 5, 1 + iInfoLine++ ), c_light_gray, + ( sTemp + " " ) ); + wprintz( w_rip, c_magenta, "%d", iTotalKills ); + + sTemp = _( "In memory of:" ); + mvwprintz( w_rip, point( FULL_SCREEN_WIDTH / 2 - utf8_width( sTemp ) / 2, iNameLine++ ), + c_light_gray, + sTemp ); + + sTemp = u.get_name(); + mvwprintz( w_rip, point( FULL_SCREEN_WIDTH / 2 - utf8_width( sTemp ) / 2, iNameLine++ ), c_white, + sTemp ); + + sTemp = _( "Last Words:" ); + mvwprintz( w_rip, point( FULL_SCREEN_WIDTH / 2 - utf8_width( sTemp ) / 2, iNameLine++ ), + c_light_gray, + sTemp ); + + int iStartX = FULL_SCREEN_WIDTH / 2 - ( ( iMaxWidth - 4 ) / 2 ); + std::string sLastWords = string_input_popup() + .window( w_rip, point( iStartX, iNameLine ), iStartX + iMaxWidth - 4 - 1 ) + .max_length( iMaxWidth - 4 - 1 ) + .query_string(); + + const bool is_suicide = uquit == QUIT_SUICIDE; + get_event_bus().send( u.getID(), u.name, u.male, is_suicide, + sLastWords ); +} + void game::death_screen() { gamemode->game_over(); diff --git a/src/game.h b/src/game.h index f04bc484e8542..dcea09eb6394f 100644 --- a/src/game.h +++ b/src/game.h @@ -951,6 +951,7 @@ class game void item_action_menu( item_location loc = item_location() ); // Displays item action menu bool is_game_over(); // Returns true if the player quit or died + void bury_screen() const;// Bury a dead character (record their last words) void death_screen(); // Display our stats, "GAME OVER BOO HOO" void draw_minimap(); // Draw the 5x5 minimap public: diff --git a/src/memorial_logger.cpp b/src/memorial_logger.cpp index 0260bb7a0c329..e9fbee8182273 100644 --- a/src/memorial_logger.cpp +++ b/src/memorial_logger.cpp @@ -877,7 +877,7 @@ void memorial_logger::notify( const cata::event &e ) } break; } - case event_type::game_over: { + case event_type::game_avatar_death: { bool suicide = e.get( "is_suicide" ); std::string last_words = e.get( "last_words" ); if( suicide ) { @@ -896,11 +896,19 @@ void memorial_logger::notify( const cata::event &e ) } break; } - case event_type::game_start: { - add( //~ %s is player name - pgettext( "memorial_male", "%s began their journey into the Cataclysm." ), - pgettext( "memorial_female", "%s began their journey into the Cataclysm." ), - avatar_name ); + case event_type::game_avatar_new: { + bool new_game = e.get( "is_new_game" ); + if( new_game ) { + add( //~ %s is player name + pgettext( "memorial_male", "%s began their journey into the Cataclysm." ), + pgettext( "memorial_female", "%s began their journey into the Cataclysm." ), + avatar_name ); + } else { + add( //~ %s is player name + pgettext( "memorial_male", "%s took over the journey through the Cataclysm." ), + pgettext( "memorial_female", "%s took over the journey through the Cataclysm." ), + avatar_name ); + } break; } case event_type::installs_cbm: { @@ -1097,7 +1105,9 @@ void memorial_logger::notify( const cata::event &e ) case event_type::cuts_tree: case event_type::reads_book: case event_type::game_load: + case event_type::game_over: case event_type::game_save: + case event_type::game_start: case event_type::u_var_changed: case event_type::vehicle_moves: break; diff --git a/src/past_games_info.cpp b/src/past_games_info.cpp index 003c66015d0f6..42557aae6b0b2 100644 --- a/src/past_games_info.cpp +++ b/src/past_games_info.cpp @@ -46,20 +46,33 @@ past_game_info::past_game_info( const JsonObject &jo ) throw JsonError( string_format( "unexpected memorial version %d", version ) ); } - // Extract the "standard" game info from the game started event - event_multiset &events = stats_->get_events( event_type::game_start ); - const event_multiset::summaries_type &counts = events.counts(); - if( counts.size() != 1 ) { - if( counts.empty() ) { - throw too_old_memorial_file_error( "memorial file lacks game_start event" ); + // Extract avatar name info from the game_avatar_new event + // gives the starting character name; "et. al." is appended if there was character switching + event_multiset &new_avatar_events = stats_->get_events( event_type::game_avatar_new ); + const event_multiset::summaries_type &new_avatar_counts = new_avatar_events.counts(); + if( !new_avatar_counts.empty() ) { + const cata::event::data_type &new_avatar_event_data = new_avatar_counts.begin()->first; + auto avatar_name_it = new_avatar_event_data.find( "avatar_name" ); + if( avatar_name_it != new_avatar_event_data.end() ) { + avatar_name_ = avatar_name_it->second.get_string() + + ( new_avatar_counts.size() > 1 ? " et. al." : "" ); + } + } else { + // Legacy approach using the game_start event + event_multiset &start_events = stats_->get_events( event_type::game_start ); + const event_multiset::summaries_type &start_counts = start_events.counts(); + if( start_counts.size() != 1 ) { + if( start_counts.empty() ) { + throw too_old_memorial_file_error( "memorial file lacks game_start event" ); + } + debugmsg( "Unexpected number of game start events: %d\n", start_counts.size() ); + return; + } + const cata::event::data_type &start_event_data = start_counts.begin()->first; + auto avatar_name_it = start_event_data.find( "avatar_name" ); + if( avatar_name_it != start_event_data.end() ) { + avatar_name_ = avatar_name_it->second.get_string(); } - debugmsg( "Unexpected number of game start events: %d\n", counts.size() ); - return; - } - const cata::event::data_type &event_data = counts.begin()->first; - auto avatar_name_it = event_data.find( "avatar_name" ); - if( avatar_name_it != event_data.end() ) { - avatar_name_ = avatar_name_it->second.get_string(); } } diff --git a/src/savegame_json.cpp b/src/savegame_json.cpp index 83ed62f2bf1b6..1570bc02ee675 100644 --- a/src/savegame_json.cpp +++ b/src/savegame_json.cpp @@ -64,6 +64,7 @@ #include "effect.h" #include "effect_source.h" #include "event.h" +#include "event_bus.h" #include "faction.h" #include "field.h" #include "field_type.h" @@ -4635,6 +4636,43 @@ void stats_tracker::deserialize( const JsonObject &jo ) d.second.set_type( d.first ); } jo.read( "initial_scores", initial_scores ); + + // TODO: remove after 0.H + // migration for saves made before addition of event_type::game_avatar_new + event_multiset gan_evts = get_events( event_type::game_avatar_new ); + if( !gan_evts.count() ) { + event_multiset gs_evts = get_events( event_type::game_start ); + if( gs_evts.count() ) { + auto gs_evt = gs_evts.first().value(); + cata::event::data_type gs_data = gs_evt.first; + + // retroactively insert starting avatar + cata::event::data_type gan_data( gs_data ); + gan_data["is_new_game"] = cata_variant::make( true ); + gan_data["is_debug"] = cata_variant::make( false ); + gan_data.erase( "game_version" ); + get_event_bus().send( cata::event( event_type::game_avatar_new, calendar::start_of_game, + std::move( gan_data ) ) ); + + // retroactively insert current avatar, if different from starting avatar + // we don't know when they took over, so just use current time point + avatar &u = get_avatar(); + if( u.getID() != gs_data["avatar_id"].get() ) { + profession_id prof_id = u.prof ? u.prof->ident() : profession::generic()->ident(); + get_event_bus().send( cata::event::make( false, false, + u.getID(), u.name, u.male, prof_id, u.custom_profession ) ); + } + } else { + // last ditch effort for really old saves that don't even have event_type::game_start + // treat current avatar as the starting avatar; abuse is_new_game=false to flag such cases + avatar &u = get_avatar(); + profession_id prof_id = u.prof ? u.prof->ident() : profession::generic()->ident(); + std::swap( calendar::turn, calendar::start_of_game ); + get_event_bus().send( cata::event::make( false, false, + u.getID(), u.name, u.male, prof_id, u.custom_profession ) ); + std::swap( calendar::turn, calendar::start_of_game ); + } + } } namespace diff --git a/tests/memorial_test.cpp b/tests/memorial_test.cpp index ba4c2edbec1f3..74cc8526e655a 100644 --- a/tests/memorial_test.cpp +++ b/tests/memorial_test.cpp @@ -219,13 +219,13 @@ TEST_CASE( "memorials", "[memorial]" ) check_memorial( m, b, "Reached skill level 8 in vehicles.", ch, skill_driving, 8 ); - check_memorial( - m, b, u_name + " was killed.\nLast words: last_words", false, "last_words", - std::chrono::seconds( 100 ) ); + check_memorial( + m, b, u_name + " was killed.\nLast words: last_words", ch, u_name, player_character.male, false, + "last_words" ); - check_memorial( - m, b, u_name + " began their journey into the Cataclysm.", ch, u_name, player_character.male, - player_character.prof->ident(), player_character.custom_profession, "VERSION_STRING" ); + check_memorial( + m, b, u_name + " began their journey into the Cataclysm.", true, false, ch, u_name, + player_character.male, player_character.prof->ident(), player_character.custom_profession ); check_memorial( m, b, "Installed bionic: Alarm System.", ch, cbm ); diff --git a/tests/stats_tracker_test.cpp b/tests/stats_tracker_test.cpp index bf8521146a0ef..a4c86fa74c916 100644 --- a/tests/stats_tracker_test.cpp +++ b/tests/stats_tracker_test.cpp @@ -205,9 +205,9 @@ TEST_CASE( "stats_tracker_event_time_bounds", "[stats]" ) static void send_game_start( event_bus &b, const character_id &u_id ) { - b.send( - u_id, "Avatar name", /*is_male=*/false, profession_id::NULL_ID(), "CUSTOM_PROFESSION", - "VERION_STRING" ); + b.send( "VERION_STRING" ); + b.send( /*is_new_game=*/true, /*is_debug=*/false, u_id, + "Avatar name", /*is_male=*/false, profession_id::NULL_ID(), "CUSTOM_PROFESSION" ); } TEST_CASE( "stats_tracker_with_event_statistics", "[stats]" ) @@ -695,7 +695,7 @@ TEST_CASE( "achievements_tracker", "[stats]" ) } else { CHECK( a.ui_text_for( &*a_kill_in_first_minute ) == "Rude awakening\n" - " Failed Year 1, Spring, day 1 0001.00\n" + " Failed Year 1, Spring, day 1 0010.00\n" " 0/1 monster killed\n" ); } @@ -709,7 +709,11 @@ TEST_CASE( "achievements_tracker", "[stats]" ) " Kill no characters\n" ); CHECK( achievements_completed.empty() ); - CHECK( achievements_failed.empty() ); + if( time_since_game_start < 1_minutes ) { + CHECK( achievements_failed.empty() ); + } else { + CHECK( achievements_failed.count( a_kill_in_first_minute ) ); + } b.send( avatar_zombie_kill ); if( time_since_game_start < 1_minutes ) {