diff --git a/data/json/effects_on_condition/mutation_eocs/mutation_effect_eocs.json b/data/json/effects_on_condition/mutation_eocs/mutation_effect_eocs.json index a025164fed4c6..0fd4be7584a0b 100644 --- a/data/json/effects_on_condition/mutation_eocs/mutation_effect_eocs.json +++ b/data/json/effects_on_condition/mutation_eocs/mutation_effect_eocs.json @@ -1,4 +1,9 @@ [ + { + "type": "effect_on_condition", + "id": "eoc_debug_mutate", + "effect": [ { "u_mutate": 0 } ] + }, { "type": "effect_on_condition", "id": "mut_eyestalk", diff --git a/data/json/mutations/mutations.json b/data/json/mutations/mutations.json index 2a3eabbb17489..312b9c52ffc0c 100644 --- a/data/json/mutations/mutations.json +++ b/data/json/mutations/mutations.json @@ -5211,6 +5211,7 @@ "prereqs": [ "WINGS_STUB" ], "prereqs2": [ "HOLLOW_BONES" ], "threshreq": [ "THRESH_BIRD" ], + "strict_threshreq": true, "category": [ "BIRD" ], "restricts_gear": [ "arm_l", "arm_r", "hand_l", "hand_r" ], "flags": [ "WINGS_2", "WING_GLIDE", "ARM_WINGS" ] @@ -8126,7 +8127,8 @@ "description": "You sometimes look back on your days before your tail came in. But you're better now.", "valid": false, "purifiable": false, - "threshold": true + "threshold": true, + "threshold_substitutes": [ "THRESH_CHIMERA" ] }, { "type": "mutation", @@ -8137,7 +8139,8 @@ "flags": [ "INVERTEBRATEBLOOD" ], "valid": false, "purifiable": false, - "threshold": true + "threshold": true, + "threshold_substitutes": [ "THRESH_CHIMERA" ] }, { "type": "mutation", @@ -8147,7 +8150,8 @@ "description": "You're sure you'll fly someday. In the meantime, there are still nests to build.", "valid": false, "purifiable": false, - "threshold": true + "threshold": true, + "threshold_substitutes": [ "THRESH_CHIMERA" ] }, { "type": "mutation", @@ -8157,7 +8161,8 @@ "description": "Ninety percent of the planet, and it's yours to explore. And colonize. And enjoy. What was that about a surface?", "valid": false, "purifiable": false, - "threshold": true + "threshold": true, + "threshold_substitutes": [ "THRESH_CHIMERA" ] }, { "type": "mutation", @@ -8167,7 +8172,8 @@ "description": "It's about time you grew out. Now that you've matured, it is time to make something of yourself.", "valid": false, "purifiable": false, - "threshold": true + "threshold": true, + "threshold_substitutes": [ "THRESH_CHIMERA" ] }, { "type": "mutation", @@ -8177,7 +8183,8 @@ "description": "Stalking prey, eating well, and lying in the sun. Mmm, all you could ever desire.", "valid": false, "purifiable": false, - "threshold": true + "threshold": true, + "threshold_substitutes": [ "THRESH_CHIMERA" ] }, { "type": "mutation", @@ -8187,7 +8194,8 @@ "description": "You're the perfect candidate to lead a pack.", "valid": false, "purifiable": false, - "threshold": true + "threshold": true, + "threshold_substitutes": [ "THRESH_CHIMERA" ] }, { "type": "mutation", @@ -8197,7 +8205,8 @@ "description": "So the humans died; what's the worry? Now they won't ruin the woods.", "valid": false, "purifiable": false, - "threshold": true + "threshold": true, + "threshold_substitutes": [ "THRESH_CHIMERA" ] }, { "type": "mutation", @@ -8207,7 +8216,8 @@ "description": "Civilization collapsed? Great! You and your kin will never have to worry about a slaughterhouse again.", "valid": false, "purifiable": false, - "threshold": true + "threshold": true, + "threshold_substitutes": [ "THRESH_CHIMERA" ] }, { "type": "mutation", @@ -8218,7 +8228,8 @@ "flags": [ "INSECTBLOOD" ], "valid": false, "purifiable": false, - "threshold": true + "threshold": true, + "threshold_substitutes": [ "THRESH_CHIMERA" ] }, { "type": "mutation", @@ -8229,7 +8240,8 @@ "flags": [ "PLANTBLOOD" ], "valid": false, "purifiable": false, - "threshold": true + "threshold": true, + "threshold_substitutes": [ "THRESH_CHIMERA" ] }, { "type": "mutation", @@ -8249,7 +8261,8 @@ "description": "Not much point to rebuilding up in that horribly bright, roofless wasteland. Now that you've become accustomed to your new digs, there's the beginning of a great empire right here, underground.", "valid": false, "purifiable": false, - "threshold": true + "threshold": true, + "threshold_substitutes": [ "THRESH_CHIMERA" ] }, { "type": "mutation", @@ -8260,7 +8273,8 @@ "flags": [ "INVERTEBRATEBLOOD" ], "valid": false, "purifiable": false, - "threshold": true + "threshold": true, + "threshold_substitutes": [ "THRESH_CHIMERA" ] }, { "type": "mutation", @@ -8271,7 +8285,8 @@ "flags": [ "INSECTBLOOD" ], "valid": false, "purifiable": false, - "threshold": true + "threshold": true, + "threshold_substitutes": [ "THRESH_CHIMERA" ] }, { "type": "mutation", @@ -8281,7 +8296,8 @@ "description": "Hey. Civilization fell. You're still around. 'Rat' just isn't respectful.", "valid": false, "purifiable": false, - "threshold": true + "threshold": true, + "threshold_substitutes": [ "THRESH_CHIMERA" ] }, { "type": "mutation", @@ -8319,6 +8335,7 @@ "name": { "str": "Chaos" }, "points": 1, "description": "You can't tell what you are anymore. Everything and yet nothing, like you weren't meant to exist. But you do, and you're a force, no matter what happens.", + "//": "Acceptable substitute for all lines except the human-based ones and slime - the former are more controlled, the latter will diverge too much in anatomy.", "valid": false, "purifiable": false, "threshold": true @@ -8331,7 +8348,8 @@ "description": "The chance to undo not one but TWO extinction events. You're confident you'll do fine.", "valid": false, "purifiable": false, - "threshold": true + "threshold": true, + "threshold_substitutes": [ "THRESH_CHIMERA" ] }, { "type": "mutation", @@ -8341,7 +8359,8 @@ "description": "A perfect ambush predator, if only there were less competition.", "valid": false, "purifiable": false, - "threshold": true + "threshold": true, + "threshold_substitutes": [ "THRESH_CHIMERA" ] }, { "type": "mutation", @@ -8351,7 +8370,8 @@ "description": "So much food, everywhere! And nobody's even guarding it anymore! These are good times.", "valid": false, "purifiable": false, - "threshold": true + "threshold": true, + "threshold_substitutes": [ "THRESH_CHIMERA" ] }, { "type": "mutation", @@ -8386,7 +8406,8 @@ "flags": [ "INVERTEBRATEBLOOD" ], "valid": false, "purifiable": false, - "threshold": true + "threshold": true, + "threshold_substitutes": [ "THRESH_CHIMERA" ] }, { "type": "mutation", @@ -8396,7 +8417,8 @@ "description": "You long for a colony, the cover of darkness, and a meal on the wing.", "valid": false, "purifiable": false, - "threshold": true + "threshold": true, + "threshold_substitutes": [ "THRESH_CHIMERA" ] }, { "type": "mutation", @@ -8884,6 +8906,17 @@ "enchantments": [ "ENCH_DEBUG_BIG_HEAD" ], "debug": true }, + { + "type": "mutation", + "id": "DEBUG_MUTATE", + "name": { "str": "Debug Genetic Instability" }, + "points": 99, + "valid": false, + "description": "You just can't wait to turn into a bug! Triggers a mutation using the normal mutation rules (requiring primer but no catalyst) every five seconds.", + "debug": true, + "processed_eocs": [ "eoc_debug_mutate" ], + "time": "5 s" + }, { "type": "mutation", "id": "DEBUG_NIGHTVISION", diff --git a/doc/MUTATIONS.md b/doc/MUTATIONS.md index ad61f3680ee98..a5fc3b13e6104 100644 --- a/doc/MUTATIONS.md +++ b/doc/MUTATIONS.md @@ -105,7 +105,9 @@ Note that **all new traits that can be obtained through mutation must be purifia "profession": true, // Trait is a starting profession special trait (default: false). "debug": false, // Trait is for debug purposes (default: false). "dummy": false, // Dummy mutations are special; they're not gained through normal mutating, and will instead be targeted for the purposes of removing conflicting mutations - "threshold": false //True if it's a threshold itself, and shouldn't be obtained *easily*. + "threshold": false, //True if it's a threshold itself, and shouldn't be obtained *easily*. Disallows mutating this trait directly + "threshold_substitutes": [ "FOO", "BAR" ], // The listed traits are accepted in place of this threshold trait for the purposes of gaining post-threshold mutations + "strict_thresreq": false, // This trait needs an *exact* threshold match (ie. ignores threshold substitutions) "player_display": true, // Trait is displayed in the `@` player display menu and mutations screen. "vanity": false, // Trait can be changed any time with no cost, like hair, eye color and skin color. "variants": [ // Cosmetic variants of this mutation. diff --git a/src/mutation.cpp b/src/mutation.cpp index 377046d82dc8e..3ada1e2f599fa 100644 --- a/src/mutation.cpp +++ b/src/mutation.cpp @@ -1636,6 +1636,19 @@ bool Character::mutate_towards( const trait_id &mut, const mutation_category_id threshreq[i].c_str() ); c_has_threshreq = true; } + for( const trait_id &subst : threshreq[i]->threshold_substitutes ) { + if( has_trait( subst ) ) { + add_msg_debug( debugmode::DF_MUTATION, "mutate_towards: substitute threshold %s found", + subst.c_str() ); + if( mdata.strict_threshreq ) { + add_msg_debug( debugmode::DF_MUTATION, + "mutate_towards: ...but no threshold substitutions allowed for trait %s", + subst.c_str(), mdata.name() ); + continue; + } + c_has_threshreq = true; + } + } } // No crossing The Threshold by simply not having it diff --git a/src/mutation.h b/src/mutation.h index 9aba84538f072..a4573809bf5e7 100644 --- a/src/mutation.h +++ b/src/mutation.h @@ -179,6 +179,10 @@ struct mutation_branch { bool purifiable = false; // True if it's a threshold itself, and shouldn't be obtained *easily* (False by default). bool threshold = false; + // Other threshold traits that are taken as acceptable replacements for this threshold + std::vector threshold_substitutes; + // Disallow threshold substitution for this trait in particular + bool strict_threshreq = false; // True if this is a trait associated with professional training/experience, so profession/quest ONLY. bool profession = false; // True if the mutation is obtained through the debug menu diff --git a/src/mutation_data.cpp b/src/mutation_data.cpp index bd3750e81e631..1ef7f1716a56f 100644 --- a/src/mutation_data.cpp +++ b/src/mutation_data.cpp @@ -367,6 +367,8 @@ void mutation_branch::load( const JsonObject &jo, const std::string &src ) } optional( jo, was_loaded, "threshold", threshold, false ); + optional( jo, was_loaded, "threshold_substitutes", threshold_substitutes ); + optional( jo, was_loaded, "strict_threshreq", strict_threshreq ); optional( jo, was_loaded, "profession", profession, false ); optional( jo, was_loaded, "debug", debug, false ); optional( jo, was_loaded, "player_display", player_display, true ); diff --git a/tests/mutation_test.cpp b/tests/mutation_test.cpp index ebfabb5105fd3..ab873941917f0 100644 --- a/tests/mutation_test.cpp +++ b/tests/mutation_test.cpp @@ -17,6 +17,7 @@ static const effect_on_condition_id effect_on_condition_changing_mutate2( "chang static const morale_type morale_perm_debug( "morale_perm_debug" ); static const mutation_category_id mutation_category_ALPHA( "ALPHA" ); +static const mutation_category_id mutation_category_BIRD( "BIRD" ); static const mutation_category_id mutation_category_CHIMERA( "CHIMERA" ); static const mutation_category_id mutation_category_FELINE( "FELINE" ); static const mutation_category_id mutation_category_HUMAN( "HUMAN" ); @@ -26,6 +27,8 @@ static const mutation_category_id mutation_category_RAPTOR( "RAPTOR" ); static const mutation_category_id mutation_category_REMOVAL_TEST( "REMOVAL_TEST" ); static const mutation_category_id mutation_category_TROGLOBITE( "TROGLOBITE" ); +static const trait_id trait_BEAK( "BEAK" ); +static const trait_id trait_BEAK_PECK( "BEAK_PECK" ); static const trait_id trait_EAGLEEYED( "EAGLEEYED" ); static const trait_id trait_FELINE_EARS( "FELINE_EARS" ); static const trait_id trait_GOURMAND( "GOURMAND" ); @@ -34,6 +37,7 @@ static const trait_id trait_QUICK( "QUICK" ); static const trait_id trait_SMELLY( "SMELLY" ); static const trait_id trait_STR_UP( "STR_UP" ); static const trait_id trait_STR_UP_2( "STR_UP_2" ); +static const trait_id trait_STR_ALPHA( "STR_ALPHA" ); static const trait_id trait_TEST_OVERMAP_SIGHT_5( "TEST_OVERMAP_SIGHT_5" ); static const trait_id trait_TEST_OVERMAP_SIGHT_MINUS_10( "TEST_OVERMAP_SIGHT_MINUS_10" ); static const trait_id trait_TEST_REMOVAL_0( "TEST_REMOVAL_0" ); @@ -42,7 +46,11 @@ static const trait_id trait_TEST_TRIGGER( "TEST_TRIGGER" ); static const trait_id trait_TEST_TRIGGER_2( "TEST_TRIGGER_2" ); static const trait_id trait_TEST_TRIGGER_2_active( "TEST_TRIGGER_2_active" ); static const trait_id trait_TEST_TRIGGER_active( "TEST_TRIGGER_active" ); +static const trait_id trait_THRESH_ALPHA( "THRESH_ALPHA" ); +static const trait_id trait_THRESH_BIRD( "THRESH_BIRD" ); +static const trait_id trait_THRESH_CHIMERA( "THRESH_CHIMERA" ); static const trait_id trait_UGLY( "UGLY" ); +static const trait_id trait_WINGS_BIRD( "WINGS_BIRD" ); static const vitamin_id vitamin_mutagen( "mutagen" ); static const vitamin_id vitamin_mutagen_human( "mutagen_human" ); @@ -723,3 +731,42 @@ TEST_CASE( "The_mutation_flags_are_associated_to_the_corresponding_base_mutation verify_mutation_flag( dummy, "COLDBLOOD4", "ECTOTHERM" ); } +// Test that threshold substitutes work +// Tested using Chimera for the mainline use case +TEST_CASE( "Threshold_substitutions", "[mutations]" ) +{ + Character &dummy = get_player_character(); + clear_avatar(); + // Check our assumptions + const std::vector bird_subst = trait_THRESH_BIRD->threshold_substitutes; + const std::vector alpha_subst = trait_THRESH_ALPHA->threshold_substitutes; + REQUIRE( std::find( bird_subst.begin(), bird_subst.end(), + trait_THRESH_CHIMERA ) != bird_subst.end() ); + REQUIRE( std::find( alpha_subst.begin(), alpha_subst.end(), + trait_THRESH_CHIMERA ) == alpha_subst.end() ); + REQUIRE( !trait_BEAK_PECK->threshreq.empty() ); + REQUIRE( !trait_WINGS_BIRD->threshreq.empty() ); + REQUIRE( !trait_BEAK_PECK->strict_threshreq ); + REQUIRE( trait_WINGS_BIRD->strict_threshreq ); + + // Character tries to gain post-thresh traits + for( int i = 0; i < 10; i++ ) { + dummy.mutate_towards( trait_BEAK_PECK, mutation_category_BIRD, nullptr, false, false ); + dummy.mutate_towards( trait_WINGS_BIRD, mutation_category_BIRD, nullptr, false, false ); + dummy.mutate_towards( trait_STR_ALPHA, mutation_category_ALPHA, nullptr, false, false ); + } + // We didn't gain any, filled up on prereqs though + CHECK( !dummy.has_trait( trait_BEAK_PECK ) ); + CHECK( !dummy.has_trait( trait_WINGS_BIRD ) ); + CHECK( !dummy.has_trait( trait_STR_ALPHA ) ); + CHECK( dummy.has_trait( trait_BEAK ) ); + // After gaining Chimera we can mutate a fancy beak, but no wings or Alpha traits + dummy.set_mutation( trait_THRESH_CHIMERA ); + dummy.mutate_towards( trait_BEAK_PECK, mutation_category_BIRD, nullptr, false, false ); + dummy.mutate_towards( trait_WINGS_BIRD, mutation_category_BIRD, nullptr, false, false ); + dummy.mutate_towards( trait_STR_ALPHA, mutation_category_ALPHA, nullptr, false, false ); + CHECK( dummy.has_trait( trait_BEAK_PECK ) ); + CHECK( !dummy.has_trait( trait_WINGS_BIRD ) ); + CHECK( !dummy.has_trait( trait_STR_ALPHA ) ); +} +