Large diffs are not rendered by default.

@@ -1,152 +1,151 @@
[
{
"type" : "mutation_category",
"id" : "mutation_any",
"id" : "ANY",
"name" : "Any",
"category" : "ANY",
"threshold_mut" : "",
"mutagen_message" : "You mutate.",
"mutagen_message" : "You feel strange.",
"iv_message" : "You inject yoursel-arRGH!",
"iv_min_mutations" : 3,
"iv_additional_mutations" : 2,
"iv_additional_mutations_chance" : 4,
"iv_fatigue" : 20,
"iv_sound" : true,
"iv_sound_message" : "You scream in agony!",
"iv_sound_message" : "yourself scream in agony!",
"iv_noise" : 15,
"iv_sleep" : true,
"iv_sleep_message" : "You writhe and collapse to the ground.",
"iv_sleep_dur" : 400,
"junkie_message" : "You quiver with anticipation..."
},{
"type" : "mutation_category",
"id" : "mutation_plant",
"id" : "PLANT",
"name" : "Plant",
"category" : "PLANT",
"threshold_mut" : "THRESH_PLANT",
"mutagen_message" : "You feel much closer to nature.",
"iv_message" : "You inject some nutrients into your phloem.",
"memorial_message" : "Bloomed forth."
},{
"type" : "mutation_category",
"id" : "mutation_insect",
"id" : "INSECT",
"name" : "Insect",
"category" : "INSECT",
"threshold_mut" : "THRESH_INSECT",
"mutagen_message" : "You hear buzzing, and feel your body harden.",
"iv_message" : "You sting yourself...for the Queen.",
"iv_message" : "You sting yourself... for the Queen.",
"memorial_message" : "Metamorphosed."
},{
"type" : "mutation_category",
"id" : "mutation_spider",
"id" : "SPIDER",
"name" : "Spider",
"category" : "SPIDER",
"threshold_mut" : "THRESH_SPIDER",
"mutagen_message" : "You feel insidious.",
"iv_message" : "Mmm...the *special* venom.",
"iv_message" : "Mmm... the *special* venom.",
"memorial_message" : "Found a place in the web of life."
},{
"type" : "mutation_category",
"id" : "mutation_slime",
"id" : "SLIME",
"name" : "Slime",
"category" : "SLIME",
"threshold_mut" : "THRESH_SLIME",
"mutagen_message" : "Your body loses all rigidity for a moment.",
"iv_message" :"This stuff takes you back. Downright primordial!",
"memorial_message" : "Gave up on rigid human norms.",
"junkie_message" : "Maybe if you drank enough, you'd become mutagen..."
},{
"type" : "mutation_category",
"id" : "mutation_fish",
"id" : "FISH",
"name" : "Fish",
"category" : "FISH",
"threshold_mut" : "THRESH_FISH",
"mutagen_message" : "You are overcome by an overwhelming longing for the ocean.",
"iv_message" : "Your pulse pounds as the waves.",
"memorial_message" : "Went deep."
},{
"type" : "mutation_category",
"id" : "mutation_rat",
"id" : "RAT",
"name" : "Rat",
"category" : "RAT",
"threshold_mut" : "THRESH_RAT",
"mutagen_message" : "You feel a momentary nausea.",
"iv_message" : "You squeak as the shot hits you.",
"iv_sound" : true,
"iv_sound_message" : "Eep!", "//" : "Sound of ratlike squeaking",
"iv_sound_message" : "eep!", "//" : "Sound of ratlike squeaking",
"iv_noise" : 1,
"memorial_message" : "Found that survival *is* everything."
},{
"type" : "mutation_category",
"id" : "mutation_beast",
"id" : "BEAST",
"name" : "Beast",
"category" : "BEAST",
"threshold_mut" : "THRESH_BEAST",
"mutagen_message" : "Your heart races and you see blood for a moment.",
"iv_message" : "Your heart races wildly as the injection takes hold.",
"memorial_message" : "Embraced his bestial nature."
},{
"type" : "mutation_category",
"id" : "mutation_bear",
"id" : "URSINE",
"name" : "Bear",
"category" : "URSINE",
"mutagen_message" : "You feel an urge to...patrol? the forests?",
"threshold_mut" : "THRESH_URSINE",
"mutagen_message" : "You feel an urge to... patrol? the forests?",
"iv_message" : "You feel yourself quite equipped for wilderness survival.",
"memorial_message" : "Became one with the bears."
},{
"type" : "mutation_category",
"id" : "mutation_cat",
"id" : "FELINE",
"name" : "Feline",
"category" : "FELINE",
"threshold_mut" : "THRESH_FELINE",
"mutagen_message" : "As you lap up the last of the mutagen, you wonder why...",
"iv_message" : "Your back arches as the mutagen takes hold.",
"memorial_message" : "Realized the dream."
},{
"type" : "mutation_category",
"id" : "mutation_wolf",
"id" : "LUPINE",
"name" : "Wolf",
"category" : "LUPINE",
"threshold_mut" : "THRESH_LUPINE",
"mutagen_message" : "You feel an urge to mark your territory. But then it passes.",
"iv_message" : "As the mutagen hits you, your ears twitch and you stifle a yipe.",
"memorial_message" : "Wolfed out."
},{
"type" : "mutation_category",
"id" : "mutation_cattle",
"id" : "CATTLE",
"name" : "Cattle", "//" : "~rBGH is a bovine growth hormone, unpopular with consumers",
"category" : "CATTLE",
"threshold_mut" : "THRESH_CATTLE",
"mutagen_message" : "Your mind and body slow down. You feel peaceful.",
"iv_message" : "You wonder if this is what rBGH feels like...",
"memorial_message" : "Stopped worrying and learned to love the cowbell."
},{
"type" : "mutation_category",
"id" : "mutation_cephalopod",
"id" : "CEPHALOPOD",
"name" : "Cephalopod", "//" : "~Zork reference, but it's talking about your blood vessels",
"category" : "CEPHALOPOD",
"threshold_mut" : "THRESH_CEPHALOPOD",
"mutagen_message" : "Your mind is overcome by images of eldritch horrors... and then they pass.",
"iv_message" : "You watch the mutagen flow through a maze of little twisty passages. All the same.",
"memorial_message" : "Began living the dreams."
},{
"type" : "mutation_category",
"id" : "mutation_bird",
"id" : "BIRD",
"name" : "Bird",
"category" : "BIRD",
"threshold_mut" : "THRESH_BIRD",
"mutagen_message" : "Your body lightens and you long for the sky.",
"iv_message" : "Your arms spasm in an oddly wavelike motion.",
"memorial_message" : "Broke free of humanity."
},{
"type" : "mutation_category",
"id" : "mutation_lizard",
"id" : "LIZARD",
"name" : "Lizard",
"category" : "LIZARD",
"threshold_mut" : "THRESH_LIZARD",
"mutagen_message" : "For a heartbeat, your blood cools down.",
"iv_message" : "Your blood cools down. The feeling is... different.",
"memorial_message" : "Shed the ugly human skin."
},{
"type" : "mutation_category",
"id" : "mutation_troglobite",
"id" : "TROGLOBITE",
"name" : "Troglobite",
"category" : "TROGLOBITE",
"threshold_mut" : "THRESH_TROGLOBITE",
"mutagen_message" : "You yearn for a cool, dark place to hide.",
"iv_message" : "As you press the plunger, it all goes so bright...",
"memorial_message" : "Adapted to underground living."
},{
"type" : "mutation_category",
"id" : "mutation_alpha",
"id" : "ALPHA",
"name" : "Alpha","//" : "5-15 pain, 66% for each of the follow-ups, so slightly better odds (designed for injection).",
"category" : "ALPHA",
"threshold_mut" : "THRESH_ALPHA",
"mutagen_message" : "You feel... better. Somehow.",
"iv_message" : "You took that shot like a champ!",
"iv_min_mutations" : 1,
@@ -159,9 +158,9 @@
"memorial_message" : "Started representing."
},{
"type" : "mutation_category",
"id" : "mutation_medical",
"id" : "MEDICAL",
"name" : "Medical","//" : "2-6 pain, same as Alpha--since specifically intended for medical applications.",
"category" : "MEDICAL",
"threshold_mut" : "THRESH_MEDICAL",
"mutagen_message" : "You can feel the blood rushing through your veins and a strange, medicated feeling washes over your senses.",
"iv_message" : "You can feel the blood in your medication stream. It's a strange feeling.",
"iv_min_mutations" : 1,
@@ -175,12 +174,12 @@
"junkie_message" : "Ahh, there it is. You can feel the mutagen again."
},{
"type" : "mutation_category",
"id" : "mutation_chimera",
"id" : "CHIMERA",
"name" : "Chimera",
"//" : "24-36 pain, Scream,, -40 Morale, but two guaranteed mutations and 75% each for third and fourth",
"category" : "CHIMERA",
"threshold_mut" : "THRESH_CHIMERA",
"mutagen_message" : "You need to roar, bask, bite, and flap. NOW.",
"iv_message" : "everyanimalthateverlived..bursting.from.YOU!",
"iv_message" : "everyanimalthateverlived.bursting.from.YOU!",
"iv_min_mutations" : 2,
"iv_additional_mutations" : 2,
"iv_additional_mutations_chance" : 4,
@@ -191,19 +190,20 @@
"iv_morale" : -40,
"iv_morale_max" : -200,
"iv_sound" : true,
"iv_sound_message" : "You roar in agony!",
"iv_sound_message" : "yourself roar in agony!!",
"iv_noise" : 25,
"iv_sleep" : true,
"iv_sleep_dur" : 800,
"iv_sleep_message" : "With a final *pop*, you go out like a light.",
"memorial_message" : "United disunity."
},{
"type" : "mutation_category",
"id" : "mutation_elf",
"id" : "ELFA",
"name" : "Fey",
"category" : "ELFA", "//" : "3-15 pain, morale boost, but no more mutagenic than cat-9s",
"threshold_mut" : "THRESH_ELFA",
"mutagen_message" : "Nature is becoming one with you...",
"iv_message" : "Everything goes green for a second.",
"//" : "3-15 pain, morale boost, but no more mutagenic than cat-9s",
"iv_pain" : 3,
"iv_morale" : 25,
"iv_morale_max" : 100,
@@ -213,11 +213,11 @@
"memorial_message" : "Accepted a more natural way of life."
},{
"type" : "mutation_category",
"id" : "mutation_raptor",
"id" : "RAPTOR",
"name" : "Raptor", "//" : "Little more painful than average, but nowhere near as harsh & effective as Chimera.",
"category" : "RAPTOR",
"mutagen_message" : "Mmm...sweet, bloody flavor...tastes like victory.",
"iv_message" : "You distinctly smell the mutagen mixing with your blood ...and then it passes.",
"threshold_mut" : "THRESH_RAPTOR",
"mutagen_message" : "Mmm... sweet, bloody flavor... tastes like victory.",
"iv_message" : "You distinctly smell the mutagen mixing with your blood... and then it passes.",
"memorial_message" : "Hatched."
}
]

Large diffs are not rendered by default.

@@ -781,7 +781,6 @@ Some armor flags, such as `WATCH` and `ALARMCLOCK` are compatible with other ite
- ```IODINE``` Adds disease `iodine`.
- ```MARLOSS``` "As you eat the berry, you have a near-religious experience, feeling at one with your surroundings..."
- ```METH``` Adds disease `meth`
- ```MUTAGEN``` Causes mutation.
- ```PKILL``` Reduces pain. Adds disease `pkill[n]` where `[n]` is the level of flag `PKILL_[n]` used on this comestible.
- ```PLANTBLECH``` Causes vomiting if player does not contain plant mutations
- ```POISON``` Adds diseases `poison` and `foodpoison`.
@@ -804,29 +803,6 @@ Some armor flags, such as `WATCH` and `ALARMCLOCK` are compatible with other ite
- ```FERTILIZER``` Works as fertilizer for farming, of if this consumed with the PLANTBLECH function penalties will be reversed for plants.
- ```HIDDEN_POISON``` ... Food is poisonous, visible only with a certain survival skill level.
- ```HIDDEN_HALLU``` ... Food causes hallucinations, visible only with a certain survival skill level.
- ```MUTAGEN_ANY``` Causes mutation in any branch.
- ```MUTAGEN_ALPHA``` Causes mutation in the alpha branch.
- ```MUTAGEN_BEAST``` Causes mutation in the beast branch.
- ```MUTAGEN_BIRD``` Causes mutation in the bird branch.
- ```MUTAGEN_CATTLE``` Causes mutation in the cattle branch.
- ```MUTAGEN_CEPHALOPOD``` Causes mutation in the cephalopod branch.
- ```MUTAGEN_CHIMERA``` Causes mutation in the chimera branch.
- ```MUTAGEN_ELFA``` Causes mutation in the elfa branch.
- ```MUTAGEN_FELINE``` Causes mutation in the feline (cat) branch.
- ```MUTAGEN_FISH``` Causes mutation in the fish branch.
- ```MUTAGEN_INSECT``` Causes mutation in the insect branch.
- ```MUTAGEN_LIZARD``` Causes mutation in the lizard branch.
- ```MUTAGEN_LUPINE``` Causes mutation in the lupine (wolf) branch.
- ```MUTAGEN_MEDICAL``` Causes mutation in the medical branch.
- ```MUTAGEN_PLANT``` Causes mutation in the plant branch.
- ```MUTAGEN_RAPTOR``` Causes mutation in the raptor branch.
- ```MUTAGEN_RAT``` Causes mutation in the rat branch.
- ```MUTAGEN_SLIME``` Causes mutation in the slime branch.
- ```MUTAGEN_SPIDER``` Causes mutation in the spider branch.
- ```MUTAGEN_TROGLOBITE``` Causes mutation in the troglobite branch.
- ```MUTAGEN_URSINE``` Causes mutation in the ursine (bear) branch.
- ```MUTAGEN_STRONG``` Chance of mutating several times.
- ```MUTAGEN_WEAK``` Causes generic mutation (with less chance to mutate than `MUTAGEN_STRONG`). Still needs one of the mutation flags from the above list.
- ```MYCUS_OK``` Can be eaten by post-threshold Mycus characters. Only applies to mycus fruits by default.
- ```PKILL_1``` Minor painkiller.
- ```PKILL_2``` Moderate painkiller.
@@ -659,11 +659,11 @@ bool player::eat( item &food, bool force )
}
if( food.has_flag( "URSINE_HONEY" ) && ( !crossed_threshold() ||
has_trait( trait_id( "THRESH_URSINE" ) ) ) &&
mutation_category_level["MUTCAT_URSINE"] > 40 ) {
//Need at least 5 bear mutations for effect to show, to filter out mutations in common with other mutcats
mutation_category_level["URSINE"] > 40 ) {
//Need at least 5 bear mutations for effect to show, to filter out mutations in common with other categories
int honey_fun = has_trait( trait_id( "THRESH_URSINE" ) ) ?
std::min( mutation_category_level["MUTCAT_URSINE"] / 8, 20 ) :
mutation_category_level["MUTCAT_URSINE"] / 12;
std::min( mutation_category_level["URSINE"] / 8, 20 ) :
mutation_category_level["URSINE"] / 12;
if( honey_fun < 10 ) {
add_msg_if_player( m_good, _( "You find the sweet taste of honey surprisingly palatable." ) );
} else {
@@ -619,8 +619,6 @@ void Item_factory::init()
add_iuse( "MP3_ON", &iuse::mp3_on );
add_iuse( "GASMASK", &iuse::gasmask );
add_iuse( "MULTICOOKER", &iuse::multicooker );
add_iuse( "MUTAGEN", &iuse::mutagen );
add_iuse( "MUT_IV", &iuse::mut_iv );
add_iuse( "MYCUS", &iuse::mycus );
add_iuse( "NOISE_EMITTER_OFF", &iuse::noise_emitter_off );
add_iuse( "NOISE_EMITTER_ON", &iuse::noise_emitter_on );
@@ -716,6 +714,8 @@ void Item_factory::init()
add_actor( new saw_barrel_actor() );
add_actor( new install_bionic_actor() );
add_actor( new detach_gunmods_actor() );
add_actor( new mutagen_actor() );
add_actor( new mutagen_iv_actor() );
// An empty dummy group, it will not spawn anything. However, it makes that item group
// id valid, so it can be used all over the place without need to explicitly check for it.
m_template_groups["EMPTY_GROUP"].reset( new Item_group( Item_group::G_COLLECTION, 100, 0, 0 ) );

Large diffs are not rendered by default.

@@ -8,6 +8,7 @@
#include "map.h"
#include "debug.h"
#include "monster.h"
#include "mutation.h"
#include "overmapbuffer.h"
#include "sounds.h"
#include "translations.h"
@@ -65,6 +66,7 @@ const efftype_id effect_recover( "recover" );
const efftype_id effect_sleep( "sleep" );
const efftype_id effect_stunned( "stunned" );
const efftype_id effect_asthma( "asthma" );
const efftype_id effect_downed( "downed" );

static const trait_id trait_CENOBITE( "CENOBITE" );
static const trait_id trait_LIGHTWEIGHT( "LIGHTWEIGHT" );
@@ -80,6 +82,7 @@ static const trait_id trait_PSYCHOPATH( "PSYCHOPATH" );
static const trait_id trait_SAPIOVORE( "SAPIOVORE" );
static const trait_id trait_SELFAWARE( "SELFAWARE" );
static const trait_id trait_TOLERANCE( "TOLERANCE" );
static const trait_id trait_MUT_JUNKIE( "MUT_JUNKIE" );

iuse_actor *iuse_transform::clone() const
{
@@ -2539,7 +2542,8 @@ bool repair_item_actor::can_repair( player &pl, const item &tool, const item &fi
}

if( &fix == &tool || any_of( materials.begin(), materials.end(), [&fix]( const material_id & mat ) {
return mat.obj().repaired_with() == fix.typeId();
return mat.obj()
.repaired_with() == fix.typeId();
} ) ) {
if( print_msg ) {
pl.add_msg_if_player( m_info, _( "This can be used to repair other items, not itself." ) );
@@ -3568,3 +3572,136 @@ void detach_gunmods_actor::finalize( const itype_id &my_item_type )
debugmsg( "Item %s has detach_gunmods_actor actor, but it's a gun.", my_item_type.c_str() );
}
}

iuse_actor *mutagen_actor::clone() const
{
return new mutagen_actor( *this );
}

void mutagen_actor::load( JsonObject &obj )
{
mutation_category = obj.get_string( "mutation_category", "ANY" );
is_weak = obj.get_bool( "is_weak", false );
is_strong = obj.get_bool( "is_strong", false );
}

long mutagen_actor::use( player &p, item &it, bool, const tripoint & ) const
{
mutagen_attempt checks = mutagen_common_checks( p, it, false,
pgettext( "memorial_male", "Consumed mutagen." ),
pgettext( "memorial_female", "Consumed mutagen." ) );

if( !checks.allowed ) {
return checks.charges_used;
}

if( is_weak && !one_in( 3 ) ) {
// Nothing! Mutagenic flesh often just fails to work.
return it.type->charges_to_use();
}

const mutation_category_trait &m_category = mutation_category_trait::get_category(
mutation_category );

if( p.has_trait( trait_MUT_JUNKIE ) ) {
p.add_msg_if_player( m_good, _( "You quiver with anticipation..." ) );
p.add_morale( MORALE_MUTAGEN, 5, 50 );
}

p.add_msg_if_player( m_category.mutagen_message.c_str() );

if( one_in( 6 ) ) {
p.add_msg_player_or_npc( m_bad,
_( "You suddenly feel dizzy, and collapse to the ground." ),
_( "<npcname> suddenly collapses to the ground!" ) );
p.add_effect( effect_downed, 1_turns, num_bp, false, 0, true );
}

int mut_count = 1 + ( is_strong ? one_in( 3 ) : 0 );

for( int i = 0; i < mut_count; i++ ) {
p.mutate_category( m_category.id );
p.mod_pain( m_category.mutagen_pain * rng( 1, 5 ) );
}

p.mod_hunger( m_category.mutagen_hunger * mut_count );
p.mod_thirst( m_category.mutagen_thirst * mut_count );
p.mod_fatigue( m_category.mutagen_fatigue * mut_count );

return it.type->charges_to_use();
}

iuse_actor *mutagen_iv_actor::clone() const
{
return new mutagen_iv_actor( *this );
}

void mutagen_iv_actor::load( JsonObject &obj )
{
mutation_category = obj.get_string( "mutation_category", "ANY" );
}

long mutagen_iv_actor::use( player &p, item &it, bool, const tripoint & ) const
{
mutagen_attempt checks = mutagen_common_checks( p, it, false,
pgettext( "memorial_male", "Injected mutagen." ),
pgettext( "memorial_female", "Injected mutagen." ) );

if( !checks.allowed ) {
return checks.charges_used;
}

const mutation_category_trait &m_category = mutation_category_trait::get_category(
mutation_category );

if( p.has_trait( trait_MUT_JUNKIE ) ) {
p.add_msg_if_player( m_category.junkie_message.c_str() );
} else {
p.add_msg_if_player( m_category.iv_message.c_str() );
}

// try to cross the threshold to be able to get post-threshold mutations this iv.
test_crossing_threshold( p, m_category );

// TODO: Remove the "is_player" part, implement NPC screams
if( p.is_player() && !( p.has_trait( trait_NOPAIN ) ) && m_category.iv_sound ) {
p.mod_pain( m_category.iv_pain );
/** @EFFECT_STR increases volume of painful shouting when using IV mutagen */
sounds::sound( p.pos(), m_category.iv_noise + p.str_cur, m_category.iv_sound_message );
}

int mut_count = m_category.iv_min_mutations;
for( int i = 0; i < m_category.iv_additional_mutations; ++i ) {
if( !one_in( m_category.iv_additional_mutations_chance ) ) {
++mut_count;
}
}

for( int i = 0; i < mut_count; i++ ) {
p.mutate_category( m_category.id );
p.mod_pain( m_category.iv_pain * rng( 1, 5 ) );
}

p.mod_hunger( m_category.iv_hunger * mut_count );
p.mod_thirst( m_category.iv_thirst * mut_count );
p.mod_fatigue( m_category.iv_fatigue * mut_count );

if( m_category.id == "CHIMERA" ) {
p.add_morale( MORALE_MUTAGEN_CHIMERA, m_category.iv_morale, m_category.iv_morale_max );
} else if( m_category.id == "ELFA" ) {
p.add_morale( MORALE_MUTAGEN_ELF, m_category.iv_morale, m_category.iv_morale_max );
} else if( m_category.iv_morale > 0 ) {
p.add_morale( MORALE_MUTAGEN_MUTATION, m_category.iv_morale, m_category.iv_morale_max );
}

if( m_category.iv_sleep && !one_in( 3 ) ) {
p.add_msg_if_player( m_bad, m_category.iv_sleep_message.c_str() );
/** @EFFECT_INT reduces sleep duration when using IV mutagen */
p.fall_asleep( time_duration::from_turns( m_category.iv_sleep_dur - p.int_cur * 5 ) );
}

// try crossing again after getting new in-category mutations.
test_crossing_threshold( p, m_category );

return it.type->charges_to_use();
}
@@ -972,4 +972,32 @@ class detach_gunmods_actor : public iuse_actor
iuse_actor *clone() const override;
void finalize( const itype_id &my_item_type ) override;
};

class mutagen_actor : public iuse_actor
{
public:
std::string mutation_category;
bool is_weak;
bool is_strong;

mutagen_actor() : iuse_actor( "mutagen" ) {}

~mutagen_actor() override = default;
void load( JsonObject &jo ) override;
long use( player &, item &, bool, const tripoint & ) const override;
iuse_actor *clone() const override;
};

class mutagen_iv_actor : public iuse_actor
{
public:
std::string mutation_category;

mutagen_iv_actor() : iuse_actor( "mutagen_iv" ) {}

~mutagen_iv_actor() override = default;
void load( JsonObject &jo ) override;
long use( player &, item &, bool, const tripoint & ) const override;
iuse_actor *clone() const override;
};
#endif
@@ -3,6 +3,8 @@
#include "action.h"
#include "game.h"
#include "map.h"
#include "item.h"
#include "itype.h"
#include "translations.h"
#include "messages.h"
#include "monster.h"
@@ -19,6 +21,8 @@

#include <algorithm>

const efftype_id effect_stunned( "stunned" );

static const trait_id trait_ROBUST( "ROBUST" );
static const trait_id trait_GLASSJAW( "GLASSJAW" );
static const trait_id trait_BURROW( "BURROW" );
@@ -34,6 +38,14 @@ static const trait_id trait_DEX_ALPHA( "DEX_ALPHA" );
static const trait_id trait_INT_ALPHA( "INT_ALPHA" );
static const trait_id trait_INT_SLIME( "INT_SLIME" );
static const trait_id trait_PER_ALPHA( "PER_ALPHA" );
static const trait_id trait_MUTAGEN_AVOID( "MUTAGEN_AVOID" );
static const trait_id trait_THRESH_MARLOSS( "THRESH_MARLOSS" );
static const trait_id trait_THRESH_MYCUS( "THRESH_MYCUS" );
static const trait_id trait_M_BLOSSOMS( "M_BLOSSOMS" );
static const trait_id trait_M_DEPENDENT( "M_DEPENDENT" );
static const trait_id trait_M_SPORES( "M_SPORES" );
static const trait_id trait_NOPAIN( "NOPAIN" );
static const trait_id trait_CARNIVORE( "CARNIVORE" );

bool Character::has_trait( const trait_id &b ) const
{
@@ -611,7 +623,7 @@ void player::mutate()

do {
// If we tried once with a non-NULL category, and couldn't find anything valid
// there, try again with MUTCAT_NULL
// there, try again with empty category
if (!first_pass) {
cat.clear();
}
@@ -661,7 +673,7 @@ void player::mutate_category( const std::string &cat )
{
// Hacky ID comparison is better than separate hardcoded branch used before
// @todo: Turn it into the null id
if( cat == "MUTCAT_ANY" ) {
if( cat == "ANY" ) {
mutate();
return;
}
@@ -1101,3 +1113,158 @@ void player::remove_child_flag( const trait_id &flag )
}
}
}

static mutagen_rejection try_reject_mutagen( player &p, const item &it, bool strong )
{
if( p.has_trait( trait_MUTAGEN_AVOID ) ) {
//~"Uh-uh" is a sound used for "nope", "no", etc.
p.add_msg_if_player( m_warning,
_( "After what happened that last time? uh-uh. You're not drinking that chemical stuff." ) );
return mutagen_rejection::rejected;
}

static const std::vector<std::string> safe = {{
"MYCUS", "MARLOSS", "MARLOSS_SEED", "MARLOSS_GEL"
}
};
if( std::any_of( safe.begin(), safe.end(), [it]( const std::string & flag ) {
return it.type->can_use( flag );
} ) ) {
return mutagen_rejection::accepted;
}

if( p.has_trait( trait_THRESH_MYCUS ) ) {
p.add_msg_if_player( m_info, _( "This is a contaminant. We reject it from the Mycus." ) );
if( p.has_trait( trait_M_SPORES ) || p.has_trait( trait_M_FERTILE ) ||
p.has_trait( trait_M_BLOSSOMS ) || p.has_trait( trait_M_BLOOM ) ) {
p.add_msg_if_player( m_good, _( "We decontaminate it with spores." ) );
g->m.ter_set( p.pos(), t_fungus );
p.add_memorial_log( pgettext( "memorial_male", "Destroyed a harmful invader." ),
pgettext( "memorial_female", "Destroyed a harmful invader." ) );
return mutagen_rejection::destroyed;
} else {
p.add_msg_if_player( m_bad,
_( "We must eliminate this contaminant at the earliest opportunity." ) );
return mutagen_rejection::rejected;
}
}

if( p.has_trait( trait_THRESH_MARLOSS ) ) {
p.add_msg_player_or_npc( m_warning,
_( "The %s sears your insides white-hot, and you collapse to the ground!" ),
_( "<npcname> writhes in agony and collapses to the ground!" ),
it.tname().c_str() );
p.vomit();
p.mod_pain( 35 + ( strong ? 20 : 0 ) );
// Lose a significant amount of HP, probably about 25-33%
p.hurtall( rng( 20, 35 ) + ( strong ? 10 : 0 ), nullptr );
// Hope you were eating someplace safe. Mycus v. Goo in your guts is no joke.
p.fall_asleep( 5_hours - 1_minutes * ( p.int_cur + ( strong ? 100 : 0 ) ) );
p.set_mutation( trait_MUTAGEN_AVOID );
// Injected mutagen purges marloss, ingested doesn't
if( strong ) {
p.unset_mutation( trait_THRESH_MARLOSS );
p.add_msg_if_player( m_warning,
_( "It was probably that marloss -- how did you know to call it \"marloss\" anyway?" ) );
p.add_msg_if_player( m_warning, _( "Best to stay clear of that alien crap in future." ) );
p.add_memorial_log( pgettext( "memorial_male",
"Burned out a particularly nasty fungal infestation." ),
pgettext( "memorial_female", "Burned out a particularly nasty fungal infestation." ) );
} else {
p.add_msg_if_player( m_warning,
_( "That was some toxic %s! Let's stick with Marloss next time, that's safe." ),
it.tname().c_str() );
p.add_memorial_log( pgettext( "memorial_male", "Suffered a toxic marloss/mutagen reaction." ),
pgettext( "memorial_female", "Suffered a toxic marloss/mutagen reaction." ) );
}

return mutagen_rejection::destroyed;
}


return mutagen_rejection::accepted;
}

mutagen_attempt mutagen_common_checks( player &p, const item &it, bool strong,
const std::string &memorial_male, const std::string &memorial_female )
{
mutagen_rejection status = try_reject_mutagen( p, it, strong );
if( status == mutagen_rejection::rejected ) {
return mutagen_attempt( false, 0 );
}
p.add_memorial_log( memorial_male.c_str(), memorial_female.c_str() );
if( status == mutagen_rejection::destroyed ) {
return mutagen_attempt( false, it.type->charges_to_use() );
}

return mutagen_attempt( true, 0 );
}

void test_crossing_threshold( player &p, const mutation_category_trait &m_category )
{
// Threshold-check. You only get to cross once!
if( p.crossed_threshold() ) {
return;
}

// If there is no threshold for this category, don't check it
const trait_id &mutation_thresh = m_category.threshold_mut;
if( mutation_thresh.is_empty() ) {
return;
}

std::string mutation_category = m_category.id;
int total = 0;
for( const auto &iter : mutation_category_trait::get_all() ) {
total += p.mutation_category_level[ iter.first ];
}
// Threshold-breaching
const std::string &primary = p.get_highest_category();
int breach_power = p.mutation_category_level[primary];
// Only if you were pushing for more in your primary category.
// You wanted to be more like it and less human.
// That said, you're required to have hit third-stage dreams first.
if( ( mutation_category == primary ) && ( breach_power > 50 ) ) {
// Little help for the categories that have a lot of crossover.
// Starting with Ursine as that's... a bear to get. 8-)
// Alpha is similarly eclipsed by other mutation categories.
// Will add others if there's serious/demonstrable need.
int booster = 0;
if( mutation_category == "URSINE" || mutation_category == "ALPHA" ) {
booster = 50;
}
int breacher = breach_power + booster;
if( x_in_y( breacher, total ) ) {
p.add_msg_if_player( m_good,
_( "Something strains mightily for a moment... and then... you're... FREE!" ) );
p.set_mutation( mutation_thresh );
p.add_memorial_log( pgettext( "memorial_male", m_category.memorial_message.c_str() ),
pgettext( "memorial_female", m_category.memorial_message.c_str() ) );
// Manually removing Carnivore, since it tends to creep in
// This is because carnivore is a prerequisite for the
// predator-style post-threshold mutations.
if( mutation_category == "URSINE" && p.has_trait( trait_CARNIVORE ) ) {
p.unset_mutation( trait_CARNIVORE );
p.add_msg_if_player( _( "Your appetite for blood fades." ) );
}
}
} else if( p.has_trait( trait_NOPAIN ) ) {
//~NOPAIN is a post-Threshold trait, so you shouldn't
//~legitimately have it and get here!
p.add_msg_if_player( m_bad, _( "You feel extremely Bugged." ) );
} else if( breach_power > 100 ) {
p.add_msg_if_player( m_bad, _( "You stagger with a piercing headache!" ) );
p.mod_pain_noresist( 8 );
p.add_effect( effect_stunned, rng( 3_turns, 5_turns ) );
} else if( breach_power > 80 ) {
p.add_msg_if_player( m_bad,
_( "Your head throbs with memories of your life, before all this..." ) );
p.mod_pain_noresist( 6 );
p.add_effect( effect_stunned, rng( 2_turns, 4_turns ) );
} else if( breach_power > 60 ) {
p.add_msg_if_player( m_bad, _( "Images of your past life flash before you." ) );
p.add_effect( effect_stunned, rng( 2_turns, 3_turns ) );
}
}


@@ -283,18 +283,11 @@ struct mutation_branch {

struct mutation_category_trait {
std::string name;
std::string id;
// Mutation category i.e "BIRD", "CHIMERA"
std::string category;
// For some reason most code uses "MUTCAT_category" instead of just "category"
// This exists only to prevent ugly string hacks
// @todo: Make this not exist
std::string category_full;
std::string id;
// The trait that you gain when you break the threshold for this category
trait_id threshold_mut;

// The flag a mutagen needs to target this category
std::string mutagen_flag;
std::string mutagen_message; // message when you consume mutagen
int mutagen_hunger = 10;//these are defaults
int mutagen_thirst = 10;
@@ -321,6 +314,7 @@ struct mutation_category_trait {
std::string memorial_message; //memorial message when you cross a threshold

static const std::map<std::string, mutation_category_trait> &get_all();
static const mutation_category_trait &get_category( std::string category_id );
static void reset();
static void check_consistency();
};
@@ -331,4 +325,21 @@ bool mutation_category_is_valid( const std::string &cat );

bool trait_display_sort( const trait_id &a, const trait_id &b ) noexcept;

enum class mutagen_rejection {
accepted,
rejected,
destroyed
};

struct mutagen_attempt {
mutagen_attempt( bool a, long c ) : allowed( a ), charges_used( c ) {}
bool allowed;
long charges_used;
};

mutagen_attempt mutagen_common_checks( player &p, const item &it, bool strong,
const std::string &memorial_male, const std::string &memorial_female );

void test_crossing_threshold( player &p, const mutation_category_trait &m_category );

#endif
@@ -79,20 +79,15 @@ void load_mutation_category(JsonObject &jsobj)
mutation_category_trait new_category;
new_category.id = jsobj.get_string("id");
new_category.name =_(jsobj.get_string("name").c_str());
new_category.category = jsobj.get_string( "category" );
// @todo: Remove
new_category.category_full = jsobj.get_string( "category_full", "MUTCAT_" + new_category.category );
// @todo: Remove default, make it required
new_category.threshold_mut = trait_id( jsobj.get_string( "threshold_mut", "THRESH_" + new_category.category ) );
new_category.mutagen_flag = jsobj.get_string( "mutagen_flag", "MUTAGEN_" + new_category.category );

new_category.mutagen_message = _(jsobj.get_string("mutagen_message", "You drink your mutagen").c_str());
new_category.threshold_mut = trait_id( jsobj.get_string( "threshold_mut" ) );

new_category.mutagen_message = _(jsobj.get_string("mutagen_message").c_str());
new_category.mutagen_hunger = jsobj.get_int("mutagen_hunger", 10);
new_category.mutagen_thirst = jsobj.get_int("mutagen_thirst", 10);
new_category.mutagen_pain = jsobj.get_int("mutagen_pain", 2);
new_category.mutagen_fatigue = jsobj.get_int("mutagen_fatigue", 5);
new_category.mutagen_morale = jsobj.get_int("mutagen_morale", 0);
new_category.iv_message = _(jsobj.get_string("iv_message", "You inject yourself").c_str());
new_category.iv_message = _(jsobj.get_string("iv_message").c_str());
new_category.iv_min_mutations = jsobj.get_int("iv_min_mutations", 1);
new_category.iv_additional_mutations = jsobj.get_int("iv_additional_mutations", 2);
new_category.iv_additional_mutations_chance = jsobj.get_int("iv_additional_mutations_chance", 3);
@@ -106,7 +101,7 @@ void load_mutation_category(JsonObject &jsobj)
new_category.iv_sound_message = _(jsobj.get_string("iv_sound_message", "You inject yoursel-arRGH!").c_str());
new_category.iv_noise = jsobj.get_int("iv_noise", 0);
new_category.iv_sleep = jsobj.get_bool("iv_sleep", false);
new_category.iv_sleep_message =_(jsobj.get_string("iv_sleep_message", "Fell asleep").c_str());
new_category.iv_sleep_message =_(jsobj.get_string("iv_sleep_message", "You fall asleep.").c_str());
new_category.iv_sleep_dur = jsobj.get_int("iv_sleep_dur", 0);
new_category.memorial_message = _(jsobj.get_string("memorial_message", "Crossed a threshold").c_str());
new_category.junkie_message = _(jsobj.get_string("junkie_message", "Oh, yeah! That's the stuff!").c_str());
@@ -119,6 +114,11 @@ const std::map<std::string, mutation_category_trait> &mutation_category_trait::g
return mutation_category_traits;
}

const mutation_category_trait &mutation_category_trait::get_category( std::string category_id )
{
return mutation_category_traits.find( category_id )->second;
}

void mutation_category_trait::reset()
{
mutation_category_traits.clear();
@@ -274,7 +274,7 @@ void mutation_branch::load( JsonObject &jsobj )
new_mut.prereqs2.emplace_back( t );
}
// Dedicated-purpose prerequisite slot for Threshold mutations
// Stuff like Huge might fit in more than one mutcat post-threshold, so yeah
// Stuff like Huge might fit in more than one category post-threshold, so yeah
for( auto &t : jsobj.get_string_array( "threshreq" ) ) {
new_mut.threshreq.emplace_back( t );
}
@@ -250,17 +250,16 @@ void npc_class::load( JsonObject &jo, const std::string & )
mutation_category_trait::get_all();
auto jo2 = jo.get_object( "mutation_rounds" );
for( auto &mutation : jo2.get_member_names() ) {
auto mutcat = "MUTCAT_" + mutation;
auto category_match = [&mutation]( std::pair<const std::string, mutation_category_trait> p ) {
return p.second.category == mutation;
return p.second.id == mutation;
};
if( std::find_if( mutation_categories.begin(), mutation_categories.end(),
category_match ) == mutation_categories.end() ) {
debugmsg( "Unrecognized mutation category %s (i.e. %s)", mutation, mutcat );
debugmsg( "Unrecognized mutation category %s", mutation );
continue;
}
auto distrib = jo2.get_object( mutation );
mutation_rounds[mutcat] = load_distribution( distrib );
mutation_rounds[mutation] = load_distribution( distrib );
}
}

@@ -168,10 +168,10 @@ static void eff_fun_rat( player &u, effect &it )
it.set_intensity( dur / 10 );
if( rng( 0, 100 ) < dur / 10 ) {
if( !one_in( 5 ) ) {
u.mutate_category( "MUTCAT_RAT" );
u.mutate_category( "RAT" );
it.mult_duration( .2 );
} else {
u.mutate_category( "MUTCAT_TROGLOBITE" );
u.mutate_category( "TROGLOBITE" );
it.mult_duration( .33 );
}
} else if( rng( 0, 100 ) < dur / 8 ) {
@@ -1078,7 +1078,7 @@ void player::hardcoded_effects( effect &it )
// Mycus folks upgrade in their sleep.
if( has_trait( trait_id( "THRESH_MYCUS" ) ) ) {
if( one_in( 8 ) ) {
mutate_category( "MUTCAT_MYCUS" );
mutate_category( "MYCUS" );
mod_hunger( 10 );
mod_thirst( 10 );
mod_fatigue( 5 );
@@ -11,7 +11,7 @@
void give_all_mutations( player &p, const mutation_category_trait &category,
bool include_postthresh )
{
const std::vector<trait_id> category_mutations = mutations_category[category.category_full];
const std::vector<trait_id> category_mutations = mutations_category[category.id];

// Add the threshold mutation first
if( include_postthresh && !category.threshold_mut.is_empty() ) {
@@ -52,9 +52,8 @@ TEST_CASE( "Having all mutations give correct highest category", "[mutations]" )
{
for( auto &cat : mutation_category_trait::get_all() ) {
const auto &cur_cat = cat.second;
const auto &cat_id = cur_cat.category;
const auto &cat_id_full = cur_cat.category_full;
if( cat_id_full == "MUTCAT_ANY" ) {
const auto &cat_id = cur_cat.id;
if( cat_id == "ANY" ) {
continue;
}

@@ -64,7 +63,7 @@ TEST_CASE( "Having all mutations give correct highest category", "[mutations]" )

THEN( cat_id + " is the strongest category" ) {
INFO( "MUTATIONS: " << get_mutations_as_string( dummy ) );
CHECK( dummy.get_highest_category() == cat_id_full );
CHECK( dummy.get_highest_category() == cat_id );
}
}

@@ -74,7 +73,7 @@ TEST_CASE( "Having all mutations give correct highest category", "[mutations]" )

THEN( cat_id + " is the strongest category" ) {
INFO( "MUTATIONS: " << get_mutations_as_string( dummy ) );
CHECK( dummy.get_highest_category() == cat_id_full );
CHECK( dummy.get_highest_category() == cat_id );
}
}
}
@@ -88,17 +87,16 @@ TEST_CASE( "Having all pre-threshold mutations gives a sensible threshold breach

for( auto &cat : mutation_category_trait::get_all() ) {
const auto &cur_cat = cat.second;
const auto &cat_id = cur_cat.category;
const auto &cat_id_full = cur_cat.category_full;
if( cat_id_full == "MUTCAT_ANY" ) {
const auto &cat_id = cur_cat.id;
if( cat_id == "ANY" ) {
continue;
}

GIVEN( "The player has all pre-threshold mutations for " + cat_id ) {
npc dummy;
give_all_mutations( dummy, cur_cat, false );

int category_strength = dummy.mutation_category_level[cat_id_full];
int category_strength = dummy.mutation_category_level[cat_id];
int total_strength = get_total_category_strength( dummy );
float breach_chance = category_strength / ( float ) total_strength;