From ac174c4e02422664f7b75db13169efd3a2a054ac Mon Sep 17 00:00:00 2001 From: eltank <8000047+eltank@users.noreply.github.com> Date: Wed, 8 Sep 2021 19:04:29 -0700 Subject: [PATCH] Take control of NPC followers --- src/avatar.cpp | 37 +++++++++++++++++++++++++++++++++++++ src/avatar.h | 12 ++++++++++++ src/character.cpp | 7 +++++++ src/character.h | 4 +++- src/debug_menu.cpp | 31 ++++++++++++++++++++++++++++++- src/debug_menu.h | 1 + src/savegame_json.cpp | 4 ++-- 7 files changed, 92 insertions(+), 4 deletions(-) diff --git a/src/avatar.cpp b/src/avatar.cpp index c75edcd124893..ed40ad0c6d094 100644 --- a/src/avatar.cpp +++ b/src/avatar.cpp @@ -145,6 +145,43 @@ avatar::avatar( avatar && ) = default; // NOLINTNEXTLINE(performance-noexcept-move-constructor) avatar &avatar::operator=( avatar && ) = default; +static void swap_npc( npc &one, npc &two, npc &tmp ) +{ + tmp = std::move( one ); + one = std::move( two ); + two = std::move( tmp ); +} + +void avatar::control_npc( npc &np ) +{ + if( !np.is_player_ally() ) { + debugmsg( "control_npc() called on non-allied npc %s", np.name ); + return; + } + if( !shadow_npc ) { + shadow_npc = std::make_unique(); + shadow_npc->op_of_u.trust = 10; + shadow_npc->op_of_u.value = 10; + shadow_npc->set_attitude( NPCATT_FOLLOW ); + } + npc tmp; + // move avatar character data into shadow npc + swap_character( *shadow_npc, tmp ); + // swap target npc with shadow npc + swap_npc( *shadow_npc, np, tmp ); + // move shadow npc character data into avatar + swap_character( *shadow_npc, tmp ); + // the avatar character is no longer a follower NPC + g->remove_npc_follower( getID() ); + // the previous avatar character is now a follower + g->add_npc_follower( np.getID() ); + np.set_fac( faction_id( "your_followers" ) ); + // perception and mutations may have changed, so reset light level caches + g->reset_light_level(); + // center the map on the new avatar character + g->update_map( *this ); +} + void avatar::toggle_map_memory() { show_map_memory = !show_map_memory; diff --git a/src/avatar.h b/src/avatar.h index 2d92c7c4a9413..55ba1ce43038e 100644 --- a/src/avatar.h +++ b/src/avatar.h @@ -103,6 +103,11 @@ class avatar : public Character return this; } + /** + * Makes the avatar "take over" the given NPC, while the current avatar character + * becomes an NPC. + */ + void control_npc( npc & ); using Character::query_yn; bool query_yn( const std::string &mes ) const override; @@ -316,6 +321,7 @@ class avatar : public Character vproto_id starting_vehicle; std::vector starting_pets; + std::set follower_ids; private: // the encumbrance on your limbs reducing your dodging ability @@ -361,6 +367,12 @@ class avatar : public Character int per_upgrade = 0; monster_visible_info mon_visible; + + /** + * The NPC that would control the avatar's character in the avatar's absence. + * The Character data in this object is not relevant/used. + */ + std::unique_ptr shadow_npc; }; avatar &get_avatar(); diff --git a/src/character.cpp b/src/character.cpp index 4da16cfcab258..630c160d241af 100644 --- a/src/character.cpp +++ b/src/character.cpp @@ -507,6 +507,13 @@ character_id Character::getID() const return this->id; } +void Character::swap_character( Character &other, Character &tmp ) +{ + tmp = std::move( other ); + other = std::move( *this ); + *this = std::move( tmp ); +} + void Character::randomize_height() { // Height distribution data is taken from CDC distributes statistics for the US population diff --git a/src/character.h b/src/character.h index 30bbbdf2060f3..6be18078e4250 100644 --- a/src/character.h +++ b/src/character.h @@ -2247,7 +2247,6 @@ class Character : public Creature, public visitable int focus_pool = 0; public: int cash = 0; - std::set follower_ids; weak_ptr_fast last_target; cata::optional last_target_pos; // Save favorite ammo location @@ -3066,6 +3065,9 @@ class Character : public Creature, public visitable Character(); Character( Character && ) noexcept( map_is_noexcept ); Character &operator=( Character && ) noexcept( list_is_noexcept ); + // Swaps the data of this Character and "other" using "tmp" for temporary storage. + // Leaves "tmp" in an undefined state. + void swap_character( Character &other, Character &tmp ); public: struct trait_data { /** Whether the mutation is activated. */ diff --git a/src/debug_menu.cpp b/src/debug_menu.cpp index d03aeeaf296cc..b57bcdd8a723b 100644 --- a/src/debug_menu.cpp +++ b/src/debug_menu.cpp @@ -147,6 +147,7 @@ std::string enum_to_string( debug_menu::debug_menu case debug_menu::debug_menu_index::LEARN_MA: return "LEARN_MA"; case debug_menu::debug_menu_index::UNLOCK_RECIPES: return "UNLOCK_RECIPES"; case debug_menu::debug_menu_index::EDIT_PLAYER: return "EDIT_PLAYER"; + case debug_menu::debug_menu_index::CONTROL_NPC: return "CONTROL_NPC"; case debug_menu::debug_menu_index::SPAWN_ARTIFACT: return "SPAWN_ARTIFACT"; case debug_menu::debug_menu_index::SPAWN_CLAIRVOYANCE: return "SPAWN_CLAIRVOYANCE"; case debug_menu::debug_menu_index::MAP_EDITOR: return "MAP_EDITOR"; @@ -237,6 +238,7 @@ static int player_uilist() { uilist_entry( debug_menu_index::DAMAGE_SELF, true, 'd', _( "Damage self" ) ) }, { uilist_entry( debug_menu_index::BLEED_SELF, true, 'b', _( "Bleed self" ) ) }, { uilist_entry( debug_menu_index::SET_AUTOMOVE, true, 'a', _( "Set automove route" ) ) }, + { uilist_entry( debug_menu_index::CONTROL_NPC, true, 'x', _( "Control NPC follower" ) ) }, }; if( !spell_type::get_all().empty() ) { uilist_initializer.emplace_back( uilist_entry( debug_menu_index::CHANGE_SPELLS, true, 'S', @@ -1126,6 +1128,29 @@ static void spawn_nested_mapgen() } } +static void control_npc_menu() +{ + std::vector> followers; + uilist charmenu; + int charnum = 0; + for( const auto &elem : g->get_follower_list() ) { + shared_ptr_fast follower = overmap_buffer.find_npc( elem ); + if( follower ) { + followers.emplace_back( follower ); + charmenu.addentry( charnum++, true, MENU_AUTOASSIGN, follower->get_name() ); + } + } + if( followers.empty() ) { + return; + } + charmenu.w_y_setup = 0; + charmenu.query(); + if( charmenu.ret < 0 || static_cast( charmenu.ret ) >= followers.size() ) { + return; + } + get_avatar().control_npc( *followers.at( charmenu.ret ) ); +} + static void character_edit_menu() { std::vector< tripoint > locations; @@ -2361,7 +2386,11 @@ void debug() break; case debug_menu_index::EDIT_PLAYER: - debug_menu::character_edit_menu(); + character_edit_menu(); + break; + + case debug_menu_index::CONTROL_NPC: + control_npc_menu(); break; case debug_menu_index::SPAWN_ARTIFACT: diff --git a/src/debug_menu.h b/src/debug_menu.h index fbb652c9fcd42..2b5638fa127cd 100644 --- a/src/debug_menu.h +++ b/src/debug_menu.h @@ -38,6 +38,7 @@ enum class debug_menu_index : int { LEARN_MA, UNLOCK_RECIPES, EDIT_PLAYER, + CONTROL_NPC, SPAWN_ARTIFACT, SPAWN_CLAIRVOYANCE, MAP_EDITOR, diff --git a/src/savegame_json.cpp b/src/savegame_json.cpp index 45d4e660b34d7..abad4450ecdb4 100644 --- a/src/savegame_json.cpp +++ b/src/savegame_json.cpp @@ -963,7 +963,6 @@ void Character::load( const JsonObject &data ) } data.read( "addictions", addictions ); - data.read( "followers", follower_ids ); // Add the earplugs. if( has_bionic( bionic_id( "bio_ears" ) ) && !has_bionic( bionic_id( "bio_earplugs" ) ) ) { @@ -1174,7 +1173,6 @@ void Character::store( JsonOut &json ) const // "Looks like I picked the wrong week to quit smoking." - Steve McCroskey json.member( "addictions", addictions ); - json.member( "followers", follower_ids ); json.member( "worn", worn ); // also saves contents json.member( "inv" ); @@ -1257,6 +1255,7 @@ void avatar::store( JsonOut &json ) const json.end_array(); } + json.member( "followers", follower_ids ); // someday, npcs may drive json.member( "controlling_vehicle", controlling_vehicle ); @@ -1346,6 +1345,7 @@ void avatar::load( const JsonObject &data ) hobbies.insert( hobbies.end(), &hobby.obj() ); } + data.read( "followers", follower_ids ); data.read( "controlling_vehicle", controlling_vehicle ); data.read( "grab_point", grab_point );