diff --git a/data/core/game_balance.json b/data/core/game_balance.json index cf3e8874c1780..7b5dce6b6307d 100644 --- a/data/core/game_balance.json +++ b/data/core/game_balance.json @@ -355,5 +355,12 @@ "info": "Sets eternal weather. Possible values are 'normal', 'clear', 'cloudy', 'light_drizzle', 'drizzle', 'rain', 'rainstorm', 'thunder', 'lightning', 'flurries', 'snowing', 'snowstorm', 'early_portal_storm', 'portal_storm'. 'normal' clears all eternal weather overrides and sets normal weather pattern. Requires restart of the game after changing value to take effect.", "stype": "string_input", "value": "normal" + }, + { + "type": "EXTERNAL_OPTION", + "name": "SHOW_MUTATION_SELECTOR", + "info": "When mutating, displays a menu which allows players to pick from a list of possible mutations.", + "stype": "bool", + "value": false } ] diff --git a/data/mods/Mutation_Selector/modinfo.json b/data/mods/Mutation_Selector/modinfo.json new file mode 100644 index 0000000000000..8864130e68783 --- /dev/null +++ b/data/mods/Mutation_Selector/modinfo.json @@ -0,0 +1,17 @@ +[ + { + "type": "MOD_INFO", + "id": "mutation_selector", + "name": "Mutation Selector", + "authors": [ "Rewryte" ], + "description": "When mutating, allows you to pick from a list of possible mutations instead of getting one at random.", + "category": "rebalance", + "dependencies": [ "dda" ] + }, + { + "type": "EXTERNAL_OPTION", + "name": "SHOW_MUTATION_SELECTOR", + "stype": "bool", + "value": true + } +] diff --git a/src/character.h b/src/character.h index 98b7f2a591792..dc923c1ee21b6 100644 --- a/src/character.h +++ b/src/character.h @@ -1468,6 +1468,9 @@ class Character : public Creature, public visitable bool allow_neutral ) const; /** Roll, based on instability, whether next mutation should be good or bad */ bool roll_bad_mutation() const; + /** Opens a menu which allows players to choose from a list of mutations */ + bool mutation_selector( const std::vector &prospective_traits, + const mutation_category_id &cat, const bool &use_vitamins ); /** Picks a random valid mutation in a category and mutate_towards() it */ void mutate_category( const mutation_category_id &mut_cat, bool use_vitamins, bool true_random = false ); diff --git a/src/mutation.cpp b/src/mutation.cpp index 3f32d5e19c2fc..93bfce977a0c2 100644 --- a/src/mutation.cpp +++ b/src/mutation.cpp @@ -33,6 +33,7 @@ #include "messages.h" #include "monster.h" #include "omdata.h" +#include "options.h" #include "output.h" #include "overmapbuffer.h" #include "pimpl.h" @@ -1069,14 +1070,24 @@ void Character::mutate( const int &true_random_chance, bool use_vitamins ) mutation_category_id cat; weighted_int_list cat_list = get_vitamin_weighted_categories(); - //If we picked bad, mutation can be bad or neutral - //Otherwise, can be good or neutral - bool picked_bad = roll_bad_mutation(); + bool select_mutation = is_avatar() && get_option( "SHOW_MUTATION_SELECTOR" ); - bool allow_good = !picked_bad; - bool allow_bad = picked_bad; + bool allow_good = false; + bool allow_bad = false; bool allow_neutral = true; + if( select_mutation ) { + // Mutation selector overrides good / bad mutation rolls + allow_good = true; + allow_bad = true; + } else if( roll_bad_mutation() ) { + // If we picked bad, mutation can be bad or neutral + allow_bad = true; + } else { + // Otherwise, can be good or neutral + allow_good = true; + } + add_msg_debug( debugmode::DF_MUTATION, "mutate: true_random_chance %d", true_random_chance ); @@ -1167,6 +1178,36 @@ void Character::mutate( const int &true_random_chance, bool use_vitamins ) } } + // Remove anything we already have, that we have a child of, that + // goes against our intention of a good/bad mutation, or that we lack resources for + for( size_t i = 0; i < valid.size(); i++ ) { + if( ( !mutation_ok( valid[i], allow_good, allow_bad, allow_neutral, mut_vit ) ) || + ( !valid[i]->valid ) ) { + add_msg_debug( debugmode::DF_MUTATION, "mutate: trait %s removed from valid trait list", + ( valid.begin() + i )->c_str() ); + valid.erase( valid.begin() + i ); + i--; + } + } + + // Mutation selector + if( select_mutation ) { + // Aggregate all prospective traits + std::vector prospective_traits; + prospective_traits.insert( prospective_traits.end(), upgrades.begin(), upgrades.end() ); + prospective_traits.insert( prospective_traits.end(), valid.begin(), valid.end() ); + for( trait_id dummy_trait : dummies ) { + // Only dummy traits with conflicts are considered + if( has_conflicting_trait( dummy_trait ) ) { + prospective_traits.push_back( dummy_trait ); + } + } + if( mutation_selector( prospective_traits, cat, use_vitamins ) ) { + // Stop if mutation properly handled by mutation selector + return; + } + } + // Prioritize upgrading existing mutations if( one_in( 2 ) ) { if( !upgrades.empty() ) { @@ -1181,18 +1222,6 @@ void Character::mutate( const int &true_random_chance, bool use_vitamins ) } } - // Remove anything we already have, that we have a child of, that - // goes against our intention of a good/bad mutation, or that we lack resources for - for( size_t i = 0; i < valid.size(); i++ ) { - if( ( !mutation_ok( valid[i], allow_good, allow_bad, allow_neutral, mut_vit ) ) || - ( !valid[i]->valid ) ) { - add_msg_debug( debugmode::DF_MUTATION, "mutate: trait %s removed from valid trait list", - ( valid.begin() + i )->c_str() ); - valid.erase( valid.begin() + i ); - i--; - } - } - // Attempt to mutate towards any dummy traits // We shuffle the list here, and try to find the first dummy trait that would be blocked by existing mutations // If we find one, we mutate towards it and stop there @@ -1248,12 +1277,24 @@ void Character::mutate_category( const mutation_category_id &cat, const bool use return; } - bool picked_bad = roll_bad_mutation(); + bool select_mutation = is_avatar() && get_option( "SHOW_MUTATION_SELECTOR" ); - bool allow_good = true_random || !picked_bad; - bool allow_bad = true_random || picked_bad; + bool allow_good = false; + bool allow_bad = false; bool allow_neutral = true; + if( select_mutation || true_random ) { + // Mutation selector and true_random overrides good / bad mutation rolls + allow_good = true; + allow_bad = true; + } else if( roll_bad_mutation() ) { + // If we picked bad, mutation can be bad or neutral + allow_bad = true; + } else { + // Otherwise, can be good or neutral + allow_good = true; + } + // Pull the category's list for valid mutations std::vector valid = mutations_category[cat]; @@ -1272,6 +1313,10 @@ void Character::mutate_category( const mutation_category_id &cat, const bool use } add_msg_debug( debugmode::DF_MUTATION, "mutate_category: mutate_towards category %s", cat.c_str() ); + if( select_mutation || mutation_selector( valid, cat, use_vitamins ) ) { + // Stop if mutation properly handled by mutation selector + return; + } mutate_towards( valid, cat, 2, use_vitamins ); } @@ -1280,6 +1325,88 @@ void Character::mutate_category( const mutation_category_id &cat ) mutate_category( cat, !mutation_category_trait::get_category( cat ).vitamin.is_null() ); } +bool Character::mutation_selector( const std::vector &prospective_traits, + const mutation_category_id &cat, const bool &use_vitamins ) +{ + // Setup menu + uilist mmenu; + mmenu.text = + _( "As your body transforms, you realize that by asserting your willpower, you can guide these changes to an extent." ); + auto make_entries = [this, &mmenu]( const std::vector &traits ) { + const size_t iterations = traits.size(); + for( int i = 0; i < static_cast( iterations ); ++i ) { + const trait_id &trait = traits[i]; + const std::string &entry_name = mutation_name( trait ); + mmenu.addentry( i, true, MENU_AUTOASSIGN, entry_name ); + } + }; + + // Only allow traits with fulfilled prerequisites + std::vector traits; + for( trait_id trait : prospective_traits ) { + const mutation_branch &mdata = trait.obj(); + + // Check prereq 1 + std::vector prereqs1 = mdata.prereqs; + bool c_has_prereq1 = prereqs1.empty() ? true : false; + for( size_t i = 0; !c_has_prereq1 && i < prereqs1.size(); i++ ) { + if( has_trait( prereqs1[i] ) ) { + c_has_prereq1 = true; + } + } + if( !c_has_prereq1 ) { + continue; + } + + // Check prereq 2 + std::vector prereqs2 = mdata.prereqs2; + bool c_has_prereq2 = prereqs2.empty() ? true : false; + for( size_t i = 0; !c_has_prereq2 && i < prereqs2.size(); i++ ) { + if( has_trait( prereqs2[i] ) ) { + c_has_prereq2 = true; + } + } + if( !c_has_prereq2 ) { + continue; + } + + // Check threshold requirement + std::vector threshreq = mdata.threshreq; + bool c_has_threshreq = threshreq.empty() ? true : false; + for( size_t i = 0; !c_has_threshreq && i < threshreq.size(); i++ ) { + if( has_trait( threshreq[i] ) ) { + c_has_threshreq = true; + } + } + if( !c_has_threshreq ) { + continue; + } + + // Check bionic conflicts + for( const bionic_id &bid : get_bionics() ) { + if( bid->mutation_conflicts.count( trait ) != 0 ) { + continue; + } + } + + // std::find function returns false on duplicate entry + if( std::find( traits.begin(), traits.end(), trait ) == traits.end() ) { + traits.push_back( trait ); + } + } + make_entries( traits ); + + // Display menu and handle selection + mmenu.query(); + if( mmenu.ret >= 0 ) { + if( mutate_towards( traits[mmenu.ret], cat, nullptr, use_vitamins ) ) { + add_msg_if_player( m_mixed, mutation_category_trait::get_category( cat ).mutagen_message() ); + } + return true; + } + return false; +} + static std::vector get_all_mutation_prereqs( const trait_id &id ) { std::vector ret;