@@ -4,6 +4,7 @@
#include <string>

#include "addiction.h"
#include "calendar.h" // ticks_between
#include "cata_utility.h"
#include "debug.h"
#include "game.h"
@@ -17,6 +18,7 @@
#include "mutation.h"
#include "options.h"
#include "output.h"
#include "stomach.h"
#include "string_formatter.h"
#include "translations.h"
#include "units.h"
@@ -342,6 +344,13 @@ int player::vitamin_mod( const vitamin_id &vit, int qty, bool capped )
return it->second;
}

void player::vitamins_mod( std::map<vitamin_id, int> vitamins, bool capped )
{
for( auto vit : vitamins ) {
vitamin_mod( vit.first, vit.second, capped );
}
}

int player::vitamin_get( const vitamin_id &vit ) const
{
if( get_option<bool>( "NO_VITAMINS" ) ) {
@@ -548,21 +557,11 @@ ret_val<edible_rating> player::will_eat( const item &food, bool interactive ) co
add_consequence( _( "Your stomach won't be happy (not rotten enough)." ), ALLERGY_WEAK );
}

const int nutr = nutrition_for( food );
const int quench = comest->quench;
const int temp_hunger = get_hunger() - nutr;
const int temp_thirst = get_thirst() - quench;

if( !has_active_mutation( trait_id( "EATHEALTH" ) ) &&
!has_active_mutation( trait_id( "HIBERNATE" ) ) &&
!has_trait( trait_id( "SLIMESPAWNER" ) ) ) {

if( get_hunger() < 0 && nutr >= 5 && !has_active_mutation( trait_id( "GOURMAND" ) ) ) {
if( stomach.stomach_remaining() < food.volume() / food.charges && !food.has_infinite_charges() ) {
if( food.has_flag( "USE_EAT_VERB" ) ) {
add_consequence( _( "You're full already and will be forcing yourself to eat." ), TOO_FULL );
} else if( ( ( nutr > 0 && temp_hunger < stomach_capacity() ) ||
( comest->quench > 0 && temp_thirst < stomach_capacity() ) ) &&
!food.has_infinite_charges() && !has_trait( trait_id( "NO_THIRST" ) ) ) {
add_consequence( _( "You will not be able to finish it all." ), TOO_FULL );
} else {
add_consequence( _( "You're full already and will be forcing yourself to drink." ), TOO_FULL );
}
}

@@ -634,10 +633,10 @@ bool player::eat( item &food, bool force )
_( "You've begun stockpiling calories and liquid for hibernation. You get the feeling that you should prepare for bed, just in case, but...you're hungry again, and you could eat a whole week's worth of food RIGHT NOW." ) );
}

const bool will_vomit = get_hunger() < 0 && nutr >= 5 && !has_trait( trait_id( "GOURMAND" ) ) &&
!hibernate &&
!has_trait( trait_id( "SLIMESPAWNER" ) ) && !has_trait( trait_id( "EATHEALTH" ) ) &&
rng( -200, 0 ) > get_hunger() - nutr;
const bool will_vomit = stomach.stomach_remaining() < food.volume() &&
This conversation was marked as resolved by KorGgenT

This comment has been minimized.

Copy link
@KorGgenT

KorGgenT Apr 5, 2019

Author Contributor

need to either use stomach_remaining or remove it as a variable

rng( units::to_milliliter( stomach.capacity() ) / 2,
units::to_milliliter( stomach.contains() ) ) > units::to_milliliter(
stomach.capacity() );
const bool saprophage = has_trait( trait_id( "SAPROPHAGE" ) );
if( spoiled && !saprophage ) {
add_msg_if_player( m_bad, _( "Ick, this %s doesn't taste so good..." ), food.tname().c_str() );
@@ -872,12 +871,8 @@ bool player::eat( item &food, bool force )
add_morale( MORALE_HONEY, honey_fun, 100 );
}

if( will_vomit ) {
vomit();
}

// chance to become parasitised
if( !( has_bionic( bio_digestion ) || has_trait( trait_id( "PARAIMMUNE" ) ) ) ) {
if( !will_vomit && !( has_bionic( bio_digestion ) || has_trait( trait_id( "PARAIMMUNE" ) ) ) ) {
if( food.get_comestible()->parasites > 0 && !food.has_flag( "NO_PARASITES" ) &&
one_in( food.get_comestible()->parasites ) ) {
switch( rng( 0, 3 ) ) {
@@ -900,56 +895,22 @@ bool player::eat( item &food, bool force )
}
}

for( const auto &v : this->vitamins_from( food ) ) {
auto qty = has_effect( effect_tapeworm ) ? v.second / 2 : v.second;

// can never develop hypervitaminosis from consuming food
vitamin_mod( v.first, qty );
if( will_vomit ) {
vomit();
}

food.mod_charges( -1 );
return true;
}

// Caps both actual nutrition/thirst and stomach capacity
void cap_nutrition_thirst( player &p, int capacity, bool food, bool water )
{
if( ( food && p.get_hunger() < capacity ) ||
( water && p.get_thirst() < capacity ) ) {
p.add_msg_if_player( _( "You can't finish it all!" ) );
}

if( p.get_hunger() < capacity ) {
p.mod_stomach_food( p.get_hunger() - capacity );
p.set_hunger( capacity );
}

if( p.get_thirst() < capacity ) {
p.mod_stomach_water( p.get_thirst() - capacity );
p.set_thirst( capacity );
}

if( p.has_trait( trait_id( "NO_THIRST" ) ) ) {
p.set_thirst( p.get_hunger() );
}

add_msg( m_debug, "%s nutrition cap: hunger %d, thirst %d, stomach food %d, stomach water %d",
p.disp_name().c_str(), p.get_hunger(), p.get_thirst(), p.get_stomach_food(),
p.get_stomach_water() );
}

void player::consume_effects( const item &food )
void player::consume_effects( item &food )
{
if( !food.is_comestible() ) {
debugmsg( "called player::consume_effects with non-comestible" );
return;
}
const auto &comest = *food.get_comestible();

const int capacity = stomach_capacity();
if( has_trait( trait_id( "THRESH_PLANT" ) ) && food.type->can_use( "PLANTBLECH" ) ) {
// Just keep nutrition capped, to prevent vomiting
cap_nutrition_thirst( *this, capacity, true, true );
// used to cap nutrition and thirst, but no longer
return;
}
if( ( has_trait( trait_id( "HERBIVORE" ) ) || has_trait( trait_id( "RUMINANT" ) ) ) &&
@@ -970,11 +931,7 @@ void player::consume_effects( const item &food )
add_msg( m_debug, "%d health from %0.2f%% rotten food", h_loss, rottedness );
}

const auto nutr = nutrition_for( food );
mod_hunger( -nutr );
mod_thirst( -comest.quench );
mod_stomach_food( nutr );
mod_stomach_water( comest.quench );
const auto nutr = nutrition_for( food ); // used in hibernation messages.
int effective_health = comest.healthy;
if( effective_health != 0 ) {
// Effectively no cap on health modifiers from food
@@ -1067,11 +1024,12 @@ void player::consume_effects( const item &food )
mod_fatigue( nutr );
}
}

// @TODO: remove this
int capacity = stomach_capacity();
// Moved here and changed a bit - it was too complex
// Incredibly minor stuff like this shouldn't require complexity
if( !is_npc() && has_trait( trait_id( "SLIMESPAWNER" ) ) &&
( get_hunger() < capacity + 40 || get_thirst() < capacity + 40 ) ) {
( get_healthy_kcal() < get_stored_kcal() + 4000 || get_thirst() < capacity + 40 ) ) {
This conversation was marked as resolved by KorGgenT

This comment has been minimized.

Copy link
@KorGgenT

KorGgenT Apr 5, 2019

Author Contributor

this might need to be > instead

This comment has been minimized.

Copy link
@KorGgenT

KorGgenT Apr 5, 2019

Author Contributor

nope, was just second guessing myself

add_msg_if_player( m_mixed,
_( "You feel as though you're going to split open! In a good way?" ) );
mod_pain( 5 );
@@ -1112,8 +1070,8 @@ void player::consume_effects( const item &food )
// Note: We want this here to prevent "you can't finish this" messages
set_hunger( capacity );
}

cap_nutrition_thirst( *this, capacity, nutr > 0, comest.quench > 0 );
// GET IN MAH BELLY!
stomach.ingest( *this, food, 1 );
}

hint_rating player::rate_action_eat( const item &it ) const
@@ -300,7 +300,7 @@ void character_edit_menu()
case D_NEEDS: {
uilist smenu;
smenu.addentry( 0, true, 'h', "%s: %d", _( "Hunger" ), p.get_hunger() );
smenu.addentry( 1, true, 's', "%s: %d", _( "Starvation" ), p.get_starvation() );
smenu.addentry( 1, true, 's', "%s: %d", _( "Stored kCal" ), p.get_stored_kcal() );
smenu.addentry( 2, true, 't', "%s: %d", _( "Thirst" ), p.get_thirst() );
smenu.addentry( 3, true, 'f', "%s: %d", _( "Fatigue" ), p.get_fatigue() );
smenu.addentry( 4, true, 'd', "%s: %d", _( "Sleep Deprivation" ), p.get_sleep_deprivation() );
@@ -320,8 +320,8 @@ void character_edit_menu()
break;

case 1:
if( query_int( value, _( "Set starvation to? Currently: %d" ), p.get_starvation() ) ) {
p.set_starvation( value );
if( query_int( value, _( "Set stored kCal to? Currently: %d" ), p.get_stored_kcal() ) ) {
p.set_stored_kcal( value );
}
break;

@@ -2989,7 +2989,16 @@ void game::debug()
}

add_msg( m_info, _( "(you: %d:%d)" ), u.posx(), u.posy() );

std::string stom =
_( "Stomach Contents: %d ml / %d ml kCal: %d, Water: %d ml" );
add_msg( m_info, stom.c_str(), units::to_milliliter( u.stomach.contains() ),
units::to_milliliter( u.stomach.capacity() ), u.stomach.get_calories(),
units::to_milliliter( u.stomach.get_water() ), u.get_hunger() );
stom = _( "Guts Contents: %d ml / %d ml kCal: %d, Water: %d ml\nHunger: %d, Thirst: %d, kCal: %d / %d" );
add_msg( m_info, stom.c_str(), units::to_milliliter( u.guts.contains() ),
units::to_milliliter( u.guts.capacity() ), u.guts.get_calories(),
units::to_milliliter( u.guts.get_water() ), u.get_hunger(), u.get_thirst(), u.get_stored_kcal(),
u.get_healthy_kcal() );
disp_NPCs();
break;
}
@@ -9309,9 +9318,10 @@ void game::eat( int pos )
return;
} else {
u.moves -= 400;
u.mod_hunger( -20 );
add_msg( _( "You eat the leaves from the %s." ), m.ter( u.pos() )->name() );
m.ter_set( u.pos(), t_grass );
add_msg( _( "You eat the underbrush." ) );
item food( "underbrush", calendar::turn, 1 );
u.eat( food );
return;
}
}
@@ -9322,8 +9332,10 @@ void game::eat( int pos )
return;
} else {
u.moves -= 400;
add_msg( _( "You graze on the %s." ), m.ter( u.pos() )->name() );
u.mod_hunger( -8 );
add_msg( _( "You eat the grass." ) );
item food( item( "grass", calendar::turn, 1 ) );
u.eat( food );
m.ter_set( u.pos(), t_dirt );
if( m.ter( u.pos() ) == t_grass_tall ) {
m.ter_set( u.pos(), t_grass_long );
} else if( m.ter( u.pos() ) == t_grass_long ) {
@@ -11619,15 +11631,15 @@ cata::optional<tripoint> game::find_or_make_stairs( map &mp, const int z_after,
add_msg( m_bad, _( "You descend on your vines, though leaving a part of you behind stings." ) );
u.mod_pain( 5 );
u.apply_damage( nullptr, bp_torso, 5 );
u.mod_hunger( 10 );
u.mod_stored_nutr( 10 );
u.mod_thirst( 10 );
} else {
add_msg( _( "You gingerly descend using your vines." ) );
}
} else {
add_msg( _( "You effortlessly lower yourself and leave a vine rooted for future use." ) );
rope_ladder = true;
u.mod_hunger( 10 );
u.mod_stored_nutr( 10 );
u.mod_thirst( 10 );
}
} else {
@@ -1421,7 +1421,8 @@ bool drink_nectar( player &p )
if( can_drink_nectar( p ) ) {
p.moves -= 50; // Takes 30 seconds
add_msg( _( "You drink some nectar." ) );
p.mod_hunger( -15 );
item nectar( "nectar", calendar::turn, 1 );
p.eat( nectar );
return true;
}

@@ -1448,7 +1449,8 @@ void iexamine::flower_poppy( player &p, const tripoint &examp )
}
p.moves -= 150; // You take your time...
add_msg( _( "You slowly suck up the nectar." ) );
p.mod_hunger( -25 );
item poppy( "poppy_nectar", calendar::turn, 1 );
p.eat( poppy );
p.mod_fatigue( 20 );
p.add_effect( effect_pkill2, 7_minutes );
// Please drink poppy nectar responsibly.
@@ -1962,8 +1964,7 @@ void iexamine::harvest_plant( player &p, const tripoint &examp )
} else if( seedType == "marloss_seed" ) {
fungus( p, examp );
g->m.i_clear( examp );
if( p.has_trait( trait_M_DEPENDENT ) && ( ( p.get_hunger() + p.get_starvation() > 500 ) ||
p.get_thirst() > 300 ) ) {
if( p.has_trait( trait_M_DEPENDENT ) && ( p.get_kcal_percent() < 0.8f || p.get_thirst() > 300 ) ) {
g->m.ter_set( examp, t_marloss );
add_msg( m_info,
_( "We have altered this unit's configuration to extract and provide local nutriment. The Mycus provides." ) );
@@ -316,7 +316,7 @@ int alcohol( player &p, const item &it, const int strength )
6_turns, 10_turns, 10_turns ) * p.str_max );
// Metabolizing the booze improves the nutritional value;
// might not be healthy, and still causes Thirst problems, though
p.mod_hunger( -( abs( it.get_comestible() ? it.get_comestible()->stim : 0 ) ) );
p.stomach.mod_nutr( -( abs( it.get_comestible() ? it.type->comestible->stim : 0 ) ) );
// Metabolizing it cancels out the depressant
p.stim += abs( it.get_comestible() ? it.get_comestible()->stim : 0 );
} else if( p.has_trait( trait_TOLERANCE ) ) {
@@ -574,7 +574,7 @@ int iuse::antiparasitic( player *p, item *it, bool, const tripoint & )
}
if( p->has_effect( effect_tapeworm ) ) {
p->remove_effect( effect_tapeworm );
p->mod_hunger( -1 ); // You just digested the tapeworm.
p->guts.mod_nutr( -1 ); // You just digested the tapeworm.
if( p->has_trait( trait_NOPAIN ) ) {
p->add_msg_if_player( m_good, _( "Your bowels clench as something inside them dies." ) );
} else {
@@ -884,10 +884,9 @@ int iuse::blech( player *p, item *it, bool, const tripoint & )
p->add_msg_if_player( m_bad, _( "Blech, that tastes gross!" ) );
//reverse the harmful values of drinking this acid.
double multiplier = -1;
p->mod_hunger( -p->nutrition_for( *it ) * multiplier );
p->stomach.mod_nutr( -p->nutrition_for( *it ) * multiplier );
p->mod_thirst( -it->get_comestible()->quench * multiplier );
p->mod_thirst( -20 ); //acidproof people can drink acids like diluted water.
p->mod_stomach_water( 20 );
p->stomach.mod_quench( 20 ); //acidproof people can drink acids like diluted water.
This conversation was marked as resolved by KorGgenT

This comment has been minimized.

Copy link
@KorGgenT

KorGgenT Apr 5, 2019

Author Contributor

this may actually need to be negative

p->mod_healthy_mod( it->get_comestible()->healthy * multiplier,
it->get_comestible()->healthy * multiplier );
p->add_morale( MORALE_FOOD_BAD, it->get_comestible()->fun * multiplier, 60, 1_hours, 30_minutes,
@@ -914,7 +913,7 @@ int iuse::plantblech( player *p, item *it, bool, const tripoint &pos )
}

//reverses the harmful values of drinking fertilizer
p->mod_hunger( p->nutrition_for( *it ) * multiplier );
p->stomach.mod_nutr( p->nutrition_for( *it ) * multiplier );
p->mod_thirst( -it->get_comestible()->quench * multiplier );
p->mod_healthy_mod( it->get_comestible()->healthy * multiplier,
it->get_comestible()->healthy * multiplier );
@@ -1007,7 +1006,7 @@ int iuse::purify_iv( player *p, item *it, bool, const tripoint & )
p->mod_pain( 2 * num_cured ); //Hurts worse as it fixes more
p->add_msg_if_player( m_warning, _( "Feels like you're on fire, but you're OK." ) );
}
p->mod_hunger( 2 * num_cured );
p->mod_stored_nutr( 2 * num_cured );
p->mod_thirst( 2 * num_cured );
p->mod_fatigue( 2 * num_cured );
}
@@ -1134,7 +1133,7 @@ static void marloss_common( player &p, item &it, const trait_id &current_color )
p.mutate();
// Gruss dich, mutation drain, missed you!
p.mod_pain( 2 * rng( 1, 5 ) );
p.mod_hunger( 10 );
p.mod_stored_nutr( 10 );
p.mod_thirst( 10 );
p.mod_fatigue( 5 );
} else if( effect <= 6 ) { // Radiation cleanse is below
@@ -1146,8 +1145,18 @@ static void marloss_common( player &p, item &it, const trait_id &current_color )
p.radiation = 0;
}
} else if( effect == 7 ) {
p.add_msg_if_player( m_good, _( "It is delicious, and very filling!" ) );
p.set_hunger( -10 );

// previously used to set hunger to -10. with the new system, needs to do something
// else that actually makes sense, so it is a little bit more involved.
units::volume fulfill_vol = std::max( p.stomach.capacity() / 8 - p.stomach.contains(), 0_ml );
if( fulfill_vol != 0_ml ) {
p.add_msg_if_player( m_good, _( "It is delicious, and very filling!" ) );
int fulfill_cal = units::to_milliliter( fulfill_vol * 6 );
p.stomach.mod_calories( fulfill_cal );
p.stomach.mod_contents( fulfill_vol );
} else {
p.add_msg_if_player( m_bad, _( "It is delicious, but you can't eat any more." ) );
}
} else if( effect == 8 ) {
p.add_msg_if_player( m_bad, _( "You take one bite, and immediately vomit!" ) );
p.vomit();
@@ -1336,7 +1345,7 @@ int iuse::mycus( player *p, item *it, bool t, const tripoint &pos )
!p->has_trait( trait_M_DEPENDENT ) ) { // OK, now set the hook.
if( !one_in( 3 ) ) {
p->mutate_category( "MYCUS" );
p->mod_hunger( 10 );
p->mod_stored_nutr( 10 );
p->mod_thirst( 10 );
p->mod_fatigue( 5 );
p->add_morale( MORALE_MARLOSS, 25, 200 ); // still covers up mutation pain
@@ -1350,7 +1359,7 @@ int iuse::mycus( player *p, item *it, bool t, const tripoint &pos )
_( "This apple tastes really weird! You're not sure it's good for you..." ) );
p->mutate();
p->mod_pain( 2 * rng( 1, 5 ) );
p->mod_hunger( 10 );
p->mod_stored_nutr( 10 );
p->mod_thirst( 10 );
p->mod_fatigue( 5 );
p->vomit(); // no hunger/quench benefit for you
@@ -4295,7 +4304,7 @@ int iuse::blood_draw( player *p, item *it, bool, const tripoint & )
if( p->has_trait( trait_ACIDBLOOD ) ) {
acid_blood = true;
}
p->mod_hunger( 10 );
p->mod_stored_nutr( 10 );
p->mod_thirst( 10 );
p->mod_pain( 3 );
}
@@ -3788,14 +3788,8 @@ long mutagen_actor::use( player &p, item &it, bool, const tripoint & ) const
p.mutate_category( m_category.id );
p.mod_pain( m_category.mutagen_pain * rng( 1, 5 ) );
}

if( m_category.mutagen_hunger * mut_count + p.get_hunger() > 300 ) {
// in this case starvation is directly updated
p.mod_starvation( m_category.mutagen_hunger * mut_count - ( 300 - p.get_hunger() ) );
p.set_hunger( 300 );
} else {
p.mod_hunger( m_category.mutagen_hunger * mut_count ) ;
}
// burn calories directly
p.mod_stored_nutr( m_category.mutagen_hunger * mut_count );
p.mod_thirst( m_category.mutagen_thirst * mut_count );
p.mod_fatigue( m_category.mutagen_fatigue * mut_count );

@@ -293,7 +293,7 @@ void player::activate_mutation( const trait_id &mut )
static item mut_ranged( weapon );
// You can take yourself halfway to Near Death levels of hunger/thirst.
// Fatigue can go to Exhausted.
if( ( mdata.hunger && get_hunger() + get_starvation() >= 700 ) || ( mdata.thirst &&
if( ( mdata.hunger && get_kcal_percent() < 0.5f ) || ( mdata.thirst &&
get_thirst() >= 260 ) ||
( mdata.fatigue && get_fatigue() >= EXHAUSTED ) ) {
// Insufficient Foo to *maintain* operation is handled in player::suffer
@@ -310,7 +310,8 @@ void player::activate_mutation( const trait_id &mut )
tdata.charge = mdata.cooldown - 1;
}
if( mdata.hunger ) {
mod_hunger( cost );
// burn some energy
mod_stored_nutr( cost );
}
if( mdata.thirst ) {
mod_thirst( cost );
@@ -290,7 +290,7 @@ void player::power_mutations()
deactivate_mutation( mut_id );
// Action done, leave screen
break;
} else if( ( !mut_data.hunger || get_hunger() <= 400 ) &&
} else if( ( !mut_data.hunger || get_kcal_percent() >= 0.8f ) &&
( !mut_data.thirst || get_thirst() <= 400 ) &&
( !mut_data.fatigue || get_fatigue() <= 400 ) ) {

Large diffs are not rendered by default.

@@ -20,6 +20,7 @@
#include "player_activity.h"
#include "ret_val.h"
#include "weighted_list.h"
#include "stomach.h"

static const std::string DEFAULT_HOTKEYS( "1234567890abcdefghijklmnopqrstuvwxyz" );

@@ -150,6 +151,7 @@ struct needs_rates {
float hunger;
float fatigue;
float recovery;
float kcal = 0.0f;
};

class player : public Character
@@ -256,6 +258,8 @@ class player : public Character
void update_body();
/** Updates all "biology" as if time between `from` and `to` passed. */
void update_body( const time_point &from, const time_point &to );
/** Updates the stomach to give accurate hunger messages */
void update_stomach( const time_point &from, const time_point &to );
/** Increases hunger, thirst, fatigue and stimulants wearing off. `rate_multiplier` is for retroactive updates. */
void update_needs( int rate_multiplier );
needs_rates calc_needs_rates();
@@ -836,6 +840,8 @@ class player : public Character
int kcal_for( const item &comest ) const;
/** Handles the nutrition value for a comestible **/
int nutrition_for( const item &comest ) const;
// easy way to get calorie value from nutrition_for
int calories_for( const item &comest ) const;
/** Handles the enjoyability value for a comestible. First value is enjoyability, second is cap. **/
std::pair<int, int> fun_for( const item &comest ) const;
/** Handles the enjoyability value for a book. **/
@@ -846,6 +852,11 @@ class player : public Character
*/
item &get_comestible_from( item &it ) const;

stomach_contents stomach;
stomach_contents guts;

void initialize_stomach_contents();

/** Get vitamin contents for a comestible */
std::map<vitamin_id, int> vitamins_from( const item &it ) const;
std::map<vitamin_id, int> vitamins_from( const itype_id &id ) const;
@@ -862,6 +873,8 @@ class player : public Character
*/
int vitamin_mod( const vitamin_id &vit, int qty, bool capped = true );

void vitamins_mod( const std::map<vitamin_id, int>, bool capped = true );

/**
* Check current level of a vitamin
*
@@ -889,7 +902,7 @@ class player : public Character
/** Current metabolic rate due to traits, hunger, speed, etc. */
float metabolic_rate() const;
/** Handles the effects of consuming an item */
void consume_effects( const item &eaten );
void consume_effects( item &eaten );
/** Handles rooting effects */
void rooted_message() const;
void rooted();
@@ -1212,8 +1225,7 @@ class player : public Character
int get_wind_resistance( body_part bp ) const;
/** Returns the effect of pain on stats */
stat_mod get_pain_penalty() const;
/** Returns the penalty to speed from hunger */
static int hunger_speed_penalty( int hunger );
int kcal_speed_penalty();
/** Returns the penalty to speed from thirst */
static int thirst_speed_penalty( int thirst );

@@ -282,12 +282,6 @@ void player::disp_info()
}
}

int starvation_speed_penalty = abs( hunger_speed_penalty( get_starvation() + get_hunger() ) );

if( get_hunger() + get_starvation() > 100 ) {
starvation_text << _( "Speed" ) << " -" << starvation_speed_penalty << "% ";
}

effect_text.push_back( starvation_text.str() );
}

@@ -684,9 +678,9 @@ Strength - 4; Dexterity - 4; Intelligence - 4; Perception - 4" ) );
( pen < 10 ? " " : "" ), pen );
line++;
}
if( get_hunger() + get_starvation() > 100 ) {
pen = abs( hunger_speed_penalty( get_hunger() + get_starvation() ) );
mvwprintz( w_speed, line, 1, c_red, _( "Under-Fed -%s%d%%" ),
if( kcal_speed_penalty() < 0 ) {
pen = abs( kcal_speed_penalty() );
mvwprintz( w_speed, line, 1, c_red, _( "Starving -%s%d%%" ),
( pen < 10 ? " " : "" ), pen );
line++;
}
@@ -231,7 +231,7 @@ static void eff_fun_hallu( player &u, effect &it )
u.add_msg_if_player( m_warning, _( "Something feels very, very wrong." ) );
}
} else if( dur > peakTime && dur < comeupTime ) {
if( u.get_stomach_food() > 0 && ( one_in( 200 ) || x_in_y( u.vomit_mod(), 50 ) ) ) {
if( u.stomach.contains() > 0_ml && ( one_in( 200 ) || x_in_y( u.vomit_mod(), 50 ) ) ) {
u.add_msg_if_player( m_bad, _( "You feel sick to your stomach." ) );
u.mod_hunger( -2 );
if( one_in( 6 ) ) {
@@ -1034,6 +1034,8 @@ void player::hardcoded_effects( effect &it )
if( g->natural_light_level( posz() ) >= 12 && compatible_weather_types ) {
if( get_hunger() >= -30 ) {
mod_hunger( -5 );
// photosynthesis warrants absorbing kcal directly
mod_stored_nutr( -5 );
}
if( get_thirst() >= -30 ) {
mod_thirst( -5 );
@@ -1085,7 +1087,7 @@ void player::hardcoded_effects( effect &it )
if( has_trait( trait_id( "THRESH_MYCUS" ) ) ) {
if( one_in( 8 ) ) {
mutate_category( "MYCUS" );
mod_hunger( 10 );
mod_stored_nutr( 10 );
mod_thirst( 10 );
mod_fatigue( 5 );
}
@@ -340,11 +340,9 @@ void Character::load( JsonObject &data )
// needs
data.read( "thirst", thirst );
data.read( "hunger", hunger );
data.read( "starvation", starvation );
data.read( "fatigue", fatigue );
data.read( "sleep_deprivation", sleep_deprivation );
data.read( "stomach_food", stomach_food );
data.read( "stomach_water", stomach_water );
data.read( "stored_calories", stored_calories );

// health
data.read( "healthy", healthy );
@@ -442,7 +440,6 @@ void Character::load( JsonObject &data )

on_stat_change( "thirst", thirst );
on_stat_change( "hunger", hunger );
on_stat_change( "starvation", starvation );
on_stat_change( "fatigue", fatigue );
on_stat_change( "sleep_deprivation", sleep_deprivation );
}
@@ -473,11 +470,9 @@ void Character::store( JsonOut &json ) const
// needs
json.member( "thirst", thirst );
json.member( "hunger", hunger );
json.member( "starvation", starvation );
json.member( "fatigue", fatigue );
json.member( "sleep_deprivation", sleep_deprivation );
json.member( "stomach_food", stomach_food );
json.member( "stomach_water", stomach_water );
json.member( "stored_calories", stored_calories );

// breathing
json.member( "underwater", underwater );
@@ -782,6 +777,9 @@ void player::serialize( JsonOut &json ) const

json.member( "vitamin_levels", vitamin_levels );

json.member( "stomach", stomach );
json.member( "guts", guts );

morale->store( json );

// mission stuff
@@ -909,6 +907,9 @@ void player::deserialize( JsonIn &jsin )
vitamin_levels[ v.first ] = lvl;
}

data.read( "stomach", stomach );
data.read( "guts", guts );

morale->load( data );

std::vector<int> tmpmissions;

Large diffs are not rendered by default.

@@ -0,0 +1,146 @@
#pragma once

#include "itype.h"
#include "game.h"
#include "player.h"
#include "string_id.h"
#include "units.h"

struct needs_rates;
class vitamin;
using vitamin_id = string_id<vitamin>;

// how much the stomach_contents passes
// based on 30 minute increments
struct stomach_pass_rates {
units::volume min_vol;
float percent_vol;
int min_kcal;
float percent_kcal;
int min_vit;
float percent_vit;
};

// how much a stomach_contents can absorb
// based on 30 minute increments
struct stomach_absorb_rates {
float percent_kcal;
int min_kcal;
std::map<vitamin_id, float> percent_vitamin;
std::map<vitamin_id, int> min_vitamin;
float percent_vitamin_default;
int min_vitamin_default;
};

// an abstract of food that has been eaten.
class stomach_contents
{
public:
stomach_contents();
stomach_contents( units::volume max_volume );

// empties the stomach_contents of all of its contents
void bowel_movement();
// empties contents equal to amount as ratio
// amount ranges from 0 to 1
void bowel_movement( stomach_pass_rates rates );
// moves @rates contents to other stomach_contents
// amount ranges from 0 to 1
void bowel_movement( stomach_pass_rates rates, stomach_contents &move_to );

// turns an item into stomach contents
// will still add contents if past maximum volume.
void ingest( player &p, item &food, int charges );

// calculates max volume for a stomach_contents
units::volume capacity() const;
// how much stomach capacity you have left before you puke from stuffing your gob
units::volume stomach_remaining() const;
// how much volume is in the stomach_contents
units::volume contains() const;

// calculates and sets absorbed kcal and vitamins
void calculate_absorbed( stomach_absorb_rates rates );

// gets the rates of passing contents out of stomach_contents if true and guts if false
stomach_pass_rates get_pass_rates( bool stomach );
// gets the absorption rates for kcal and vitamins
// stomach == true, guts == false
stomach_absorb_rates get_absorb_rates( bool stomach, needs_rates metabolic_rates );

int get_calories() const;
int get_calories_absorbed() const;
void set_calories_absorbed( int cal );
units::volume get_water() const;

// changes calorie amount
void mod_calories( int calories );
// sets calories amount
void set_calories( int cal );

// changes calorie amount based on old nutr value
void mod_nutr( int nutr );
// changes water amount in stomach
// overflow draws from player thirst
void mod_water( units::volume h2o );
// changes water amount in stomach converted from quench value
// @TODO: Move to mL values of water
void mod_quench( int quench );
// adds volume to your stomach
void mod_contents( units::volume vol );

void absorb_water( player &p, units::volume amount );

// moves absorbed nutrients to the player for use
// returns true if any calories are absorbed
// does not empty absorbed calories
bool store_absorbed( player &p );

// how long has it been since i ate?
// only really relevant for player::stomach
time_duration time_since_ate() const;

void serialize( JsonOut &json ) const;
void deserialize( JsonIn &json );

private:

// vitamins in stomach_contents
std::map<vitamin_id, int> vitamins;
// vitamins to be absorbed in stomach_contents
std::map<vitamin_id, int> vitamins_absorbed;
// number of calories in stomach_contents
int calories = 0;
// number of calories to be absorbed in stomach_contents
int calories_absorbed = 0;
// volume of water in stomach_contents
units::volume water;
/**
* this is the maximum volume without modifiers such as mutations
* in order to get the maximum volume with all modifiers properly,
* call stomach_capacity()
*/
units::volume max_volume;
// volume of food in stomach_contents
units::volume contents;
// when did this stomach_contents call stomach_contents::ingest()
time_point last_ate;

// turns calories into absorbed calories.
// they are not added to your fat stores just yet
// only does anything if the input is a positive number
void absorb_kcal( int amount );
// absorbs a single vitamin.
// does not add it to player vitamins yet
// returns true if vitamins are absorbed
bool absorb_vitamin( vitamin_id vit, int amount );
// absorbs a single vitamin
// does not add it to player vitamins yet
// returns true if vitamins are absorbed
bool absorb_vitamin( std::pair<vitamin_id, int> vit );
// absorbs multiple vitamins
// does not add it to player vitamins yet
// returns true if any vitamins are absorbed
bool absorb_vitamins( std::map<vitamin_id, int> vitamins );

};
@@ -3725,7 +3725,7 @@ void vehicle::consume_fuel( int load, const int t_seconds, bool skip_electric )
g->u.charge_power( -10 );
}
if( one_in( 10 ) ) {
g->u.mod_hunger( mod );
g->u.mod_stored_nutr( mod );
g->u.mod_thirst( mod );
g->u.mod_fatigue( mod );
}
@@ -0,0 +1,287 @@
#include "catch/catch.hpp"
#include "calendar.h"
#include "crafting.h"
#include "game.h"
#include "itype.h"
#include "map_helpers.h"
#include "npc.h"
#include "player.h"
#include "player_helpers.h"
#include "test_statistics.h"
#include "item.h"

void reset_time()
{
calendar::turn = calendar( 0 );
player &p = g->u;
p.set_stored_kcal( p.get_healthy_kcal() );
p.set_hunger( 0 );
clear_player();
}

void pass_time( player &p, time_duration amt )
{
for( auto turns = 1_turns; turns < amt; turns += 1_turns ) {
calendar::turn.increment();
p.update_body();
}
}

void clear_stomach( player &p )
{
p.guts.set_calories( 0 );
p.stomach.set_calories( 0 );
p.stomach.bowel_movement();
p.guts.bowel_movement();
}

void set_all_vitamins( int target, player &p )
{
p.vitamin_set( vitamin_id( "vitA" ), target );
p.vitamin_set( vitamin_id( "vitB" ), target );
p.vitamin_set( vitamin_id( "vitC" ), target );
p.vitamin_set( vitamin_id( "iron" ), target );
p.vitamin_set( vitamin_id( "calcium" ), target );
}

// time (in minutes) it takes for the player to feel hungry
// passes time on the calendar
time_duration time_until_hungry( player &p )
{
unsigned int thirty_minutes = 0;
do {
pass_time( p, 30_minutes );
thirty_minutes++;
} while( p.get_hunger() < 40 ); // hungry
return thirty_minutes * 30_minutes;
}

void print_stomach_contents( player &p, const bool print )
{
if( !print ) {
return;
}
printf( "stomach: %d/%d guts: %d/%d player: %d/%d hunger: %d\n", p.stomach.get_calories(),
p.stomach.get_calories_absorbed(), p.guts.get_calories(),
p.guts.get_calories_absorbed(), p.get_stored_kcal(), p.get_healthy_kcal(), p.get_hunger() );
printf( "stomach: %d mL/ %d mL guts %d mL/ %d mL\n",
units::to_milliliter<int>( p.stomach.contains() ),
units::to_milliliter<int>( p.stomach.capacity() ),
units::to_milliliter<int>( p.guts.contains() ),
units::to_milliliter<int>( p.guts.capacity() ) );
printf( "metabolic rate: %.2f\n", p.metabolic_rate() );
}

// this represents an amount of food you can eat to keep you fed for an entire day
// accounting for appropriate vitamins
void eat_all_nutrients( player &p )
{
item f( "pizza_veggy" );
p.eat( f );
f = item( "pizza_veggy" );
p.eat( f );
f = item( "pizza_veggy" );
p.eat( f );
f = item( "pizza_veggy" );
p.eat( f );
f = item( "fried_livers" );
p.eat( f );
f = item( "chips3" );
p.eat( f );
f = item( "chips3" );
p.eat( f );
f = item( "chips3" );
p.eat( f );
f = item( "chips3" );
p.eat( f );
}

// how long does it take to starve to death
// player does not thirst or tire or require vitamins
TEST_CASE( "starve_test" )
{
// change this bool when editing the test
const bool print_tests = false;
player &dummy = g->u;
reset_time();
clear_stomach( dummy );
if( print_tests ) {
printf( "\n\n" );
}
unsigned int day = 0;
do {
if( print_tests ) {
printf( "day %d: %d\n", day, dummy.get_stored_kcal() );
}
pass_time( dummy, 1_days );
dummy.set_thirst( 0 );
dummy.set_fatigue( 0 );
set_all_vitamins( 0, dummy );
day++;
} while( dummy.get_stored_kcal() > 0 );
if( print_tests ) {
printf( "\n\n" );

CHECK( day == 46 );
}
}

// how long does it take to starve to death with extreme metabolism
// player does not thirst or tire or require vitamins
TEST_CASE( "starve_test_hunger3" )
{
// change this bool when editing the test
const bool print_tests = false;
player &dummy = g->u;
reset_time();
clear_stomach( dummy );
while( !( dummy.has_trait( trait_id( "HUNGER3" ) ) ) ) {
dummy.mutate_towards( trait_id( "HUNGER3" ) );
}
clear_stomach( dummy );
if( print_tests ) {
printf( "\n\n" );
}
unsigned int day = 0;
do {
if( print_tests ) {
printf( "day %d: %d\n", day, dummy.get_stored_kcal() );
}
pass_time( dummy, 1_days );
dummy.set_thirst( 0 );
dummy.set_fatigue( 0 );
set_all_vitamins( 0, dummy );
day++;
} while( dummy.get_stored_kcal() > 0 );
if( print_tests ) {
printf( "\n\n" );
}
CHECK( day <= 15 );
CHECK( day >= 14 );
}

// does eating enough food per day keep you alive
TEST_CASE( "all_nutrition_starve_test" )
{
// change this bool when editing the test
const bool print_tests = false;
player &dummy = g->u;
reset_time();
clear_stomach( dummy );
eat_all_nutrients( dummy );
if( print_tests ) {
printf( "\n\n" );
}

for( unsigned int day = 0; day <= 10; day++ ) {
if( print_tests ) {
printf( "day %d: %d\n", day, dummy.get_stored_kcal() );
}
pass_time( dummy, 1_days );
dummy.set_thirst( 0 );
dummy.set_fatigue( 0 );
eat_all_nutrients( dummy );
print_stomach_contents( dummy, print_tests );
}
if( print_tests ) {
printf( "vitamins: vitA %d vitB %d vitC %d calcium %d iron %d\n",
dummy.vitamin_get( vitamin_id( "vitA" ) ), dummy.vitamin_get( vitamin_id( "vitB" ) ),
dummy.vitamin_get( vitamin_id( "vitC" ) ), dummy.vitamin_get( vitamin_id( "calcium" ) ),
dummy.vitamin_get( vitamin_id( "iron" ) ) );
printf( "\n\n" );
}
CHECK( dummy.get_stored_kcal() >= dummy.get_healthy_kcal() );
// since vitamins drain very quickly, it is almost impossible to remain at 0
CHECK( dummy.vitamin_get( vitamin_id( "vitA" ) ) >= -2 );
CHECK( dummy.vitamin_get( vitamin_id( "vitB" ) ) >= -2 );
CHECK( dummy.vitamin_get( vitamin_id( "vitC" ) ) >= -2 );
CHECK( dummy.vitamin_get( vitamin_id( "iron" ) ) >= -2 );
CHECK( dummy.vitamin_get( vitamin_id( "calcium" ) ) >= -2 );
}

// reasonable length of time to pass before hunger sets in
TEST_CASE( "hunger" )
{
// change this bool when editing the test
const bool print_tests = false;
player &dummy = g->u;
reset_time();
clear_stomach( dummy );
dummy.initialize_stomach_contents();

if( print_tests ) {
printf( "\n\n" );
}
print_stomach_contents( dummy, print_tests );
int hunger_time = to_minutes<int>( time_until_hungry( dummy ) );
if( print_tests ) {
printf( "%d minutes til hunger sets in\n", hunger_time );
print_stomach_contents( dummy, print_tests );
printf( "eat 2 cooked meat\n" );
}
CHECK( hunger_time <= 270 );
CHECK( hunger_time >= 240 );
item f( "meat_cooked" );
dummy.eat( f );
f = item( "meat_cooked" );
dummy.eat( f );
dummy.set_thirst( 0 );
dummy.update_body();
print_stomach_contents( dummy, print_tests );
hunger_time = to_minutes<int>( time_until_hungry( dummy ) );
if( print_tests ) {
printf( "%d minutes til hunger sets in\n", hunger_time );
print_stomach_contents( dummy, print_tests );
printf( "eat 2 beansnrice\n" );
}
CHECK( hunger_time <= 240 );
CHECK( hunger_time >= 210 );
f = item( "beansnrice" );
dummy.eat( f );
f = item( "beansnrice" );
dummy.eat( f );
dummy.update_body();
print_stomach_contents( dummy, print_tests );
hunger_time = to_minutes<int>( time_until_hungry( dummy ) );
if( print_tests ) {
printf( "%d minutes til hunger sets in\n", hunger_time );
}
CHECK( hunger_time <= 240 );
CHECK( hunger_time >= 210 );
if( print_tests ) {
print_stomach_contents( dummy, print_tests );
printf( "eat 16 veggy\n" );
}
for( int i = 0; i < 16; i++ ) {
f = item( "veggy" );
dummy.eat( f );
}
dummy.update_body();
print_stomach_contents( dummy, print_tests );
hunger_time = to_minutes<int>( time_until_hungry( dummy ) );
if( print_tests ) {
printf( "%d minutes til hunger sets in\n", hunger_time );
print_stomach_contents( dummy, print_tests );
}
CHECK( hunger_time <= 300 );
CHECK( hunger_time >= 240 );
if( print_tests ) {
printf( "eat 16 veggy with extreme metabolism\n" );
}
while( !( dummy.has_trait( trait_id( "HUNGER3" ) ) ) ) {
dummy.mutate_towards( trait_id( "HUNGER3" ) );
}
for( int i = 0; i < 16; i++ ) {
f = item( "veggy" );
dummy.eat( f );
}
dummy.update_body();
print_stomach_contents( dummy, print_tests );
hunger_time = to_minutes<int>( time_until_hungry( dummy ) );
if( print_tests ) {
printf( "%d minutes til hunger sets in\n", hunger_time );
print_stomach_contents( dummy, print_tests );
}
CHECK( hunger_time <= 210 );
CHECK( hunger_time >= 120 );
}