diff --git a/data/json/items/comestibles/meat_dishes.json b/data/json/items/comestibles/meat_dishes.json index 16d8ced2eb02c..aae826d4f9b62 100644 --- a/data/json/items/comestibles/meat_dishes.json +++ b/data/json/items/comestibles/meat_dishes.json @@ -400,6 +400,16 @@ "fun": -8, "vitamins": [ [ "iron", 3 ] ] }, + { + "type": "COMESTIBLE", + "id": "debug_nutrition", + "copy-from": "can_spam", + "name": "holy SPAM of debugging", + "calories": 2100, + "description": "A mysterious lump of SPAM that contains just enough calories and vitamins to feed you for a day. For debug use only.", + "//": "This is used for the all_nutrient_starve_test.", + "vitamins": [ [ "vitA", 96 ], [ "vitB", 96 ], [ "vitC", 96 ], [ "calcium", 96 ], [ "iron", 96 ] ] + }, { "type": "COMESTIBLE", "id": "can_sardine", diff --git a/src/character.cpp b/src/character.cpp index 9b58cebb9a60d..161173371dd3d 100644 --- a/src/character.cpp +++ b/src/character.cpp @@ -5219,8 +5219,7 @@ void Character::vomit() g->events().send( getID() ); if( stomach.contains() != 0_ml ) { - // empty stomach contents - stomach.bowel_movement(); + stomach.empty(); g->m.add_field( adjacent_tile(), fd_bile, 1 ); add_msg_player_or_npc( m_bad, _( "You throw up heavily!" ), _( " throws up heavily!" ) ); } diff --git a/src/consumption.cpp b/src/consumption.cpp index 7559f03ea5a98..571e7b086dd8c 100644 --- a/src/consumption.cpp +++ b/src/consumption.cpp @@ -597,7 +597,7 @@ ret_val player::will_eat( const item &food, bool interactive ) co add_consequence( _( "Your stomach won't be happy (not rotten enough)." ), ALLERGY_WEAK ); } - if( food.charges > 0 && stomach.stomach_remaining() < food.volume() / food.charges && + if( food.charges > 0 && stomach.stomach_remaining( *this ) < food.volume() / food.charges && !food.has_infinite_charges() ) { if( edible ) { add_consequence( _( "You're full already and will be forcing yourself to eat." ), TOO_FULL ); @@ -679,10 +679,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 = stomach.stomach_remaining() < food.volume() && - rng( units::to_milliliter( stomach.capacity() ) / 2, + const bool will_vomit = stomach.stomach_remaining( *this ) < food.volume() && + rng( units::to_milliliter( stomach.capacity( *this ) ) / 2, units::to_milliliter( stomach.contains() ) ) > units::to_milliliter( - stomach.capacity() ); + stomach.capacity( *this ) ); 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() ); @@ -1143,8 +1143,21 @@ bool player::consume_effects( item &food ) // Note: We want this here to prevent "you can't finish this" messages set_hunger( capacity ); } + + // Set up food for ingestion + nutrients ingested; + const item &contained_food = food.is_container() ? food.get_contained() : food; + // maybe move tapeworm to digestion + for( const std::pair &v : vitamins_from( contained_food ) ) { + ingested.vitamins[v.first] += has_effect( efftype_id( "tapeworm" ) ) ? v.second / 2 : v.second; + } + // @TODO: Move quench values to mL and remove the magic number here + ingested.water = contained_food.type->comestible->quench * 5_ml; + ingested.solids = contained_food.base_volume() - std::max( ingested.water, 0_ml ); + ingested.kcal = kcal_for( contained_food ); + // GET IN MAH BELLY! - stomach.ingest( *this, food, 1 ); + stomach.ingest( ingested ); return true; } diff --git a/src/debug_menu.cpp b/src/debug_menu.cpp index 7b23ea9705f02..b426d1bd97579 100644 --- a/src/debug_menu.cpp +++ b/src/debug_menu.cpp @@ -1115,11 +1115,11 @@ void debug() 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.capacity( u ) ), 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.capacity( u ) ), 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() ); add_msg( m_info, _( "Body Mass Index: %.0f\nBasal Metabolic Rate: %i" ), u.get_bmi(), u.get_bmr() ); diff --git a/src/iuse.cpp b/src/iuse.cpp index 285fdc0fa7b18..ebf439cf182ef 100644 --- a/src/iuse.cpp +++ b/src/iuse.cpp @@ -1250,7 +1250,7 @@ static void marloss_common( player &p, item &it, const trait_id ¤t_color ) // 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 ); + units::volume fulfill_vol = std::max( p.stomach.capacity( p ) / 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 ); diff --git a/src/player.cpp b/src/player.cpp index 7eb40decc945e..605042358511f 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -2491,37 +2491,39 @@ void player::update_stomach( const time_point &from, const time_point &to ) const bool mycus = has_trait( trait_M_DEPENDENT ); const float kcal_per_time = get_bmr() / ( 12.0f * 24.0f ); const int five_mins = ticks_between( from, to, 5_minutes ); + const int half_hours = ticks_between( from, to, 30_minutes ); + const units::volume stomach_capacity = stomach.capacity( *this ); if( five_mins > 0 ) { - stomach.absorb_water( *this, 250_ml * five_mins ); - guts.absorb_water( *this, 250_ml * five_mins ); - } - if( ticks_between( from, to, 30_minutes ) > 0 ) { - // the stomach does not currently have rates of absorption, but this is where it goes - stomach.calculate_absorbed( stomach.get_absorb_rates( true, rates ) ); - guts.calculate_absorbed( guts.get_absorb_rates( false, rates ) ); - stomach.store_absorbed( *this ); - guts.store_absorbed( *this ); - guts.bowel_movement( guts.get_pass_rates( false ) ); - stomach.bowel_movement( stomach.get_pass_rates( true ), guts ); + // Digest nutrients in stomach, they are destined for the guts (except water) + nutrients digested_to_guts = stomach.digest( *this, rates, five_mins, half_hours ); + // Digest nutrients in guts, they will be distributed to needs levels + nutrients digested_to_body = guts.digest( *this, rates, five_mins, half_hours ); + // Water from stomach skips guts and gets absorbed by body + set_thirst( std::max( + -100, get_thirst() - units::to_milliliter( digested_to_guts.water ) / 5 ) ); + guts.ingest( digested_to_guts ); + // Apply nutrients, unless this is an NPC and NO_NPC_FOOD is enabled. + if( !is_npc() || !get_option( "NO_NPC_FOOD" ) ) { + mod_stored_kcal( digested_to_body.kcal ); + vitamins_mod( digested_to_body.vitamins, false ); + } } if( stomach.time_since_ate() > 10_minutes ) { - if( stomach.contains() >= stomach.capacity() && get_hunger() > -61 ) { + if( stomach.contains() >= stomach_capacity && get_hunger() > -61 ) { // you're engorged! your stomach is full to bursting! set_hunger( -61 ); - } else if( stomach.contains() >= stomach.capacity() / 2 && get_hunger() > -21 ) { + } else if( stomach.contains() >= stomach_capacity / 2 && get_hunger() > -21 ) { // sated set_hunger( -21 ); - } else if( stomach.contains() >= stomach.capacity() / 8 && get_hunger() > -1 ) { + } else if( stomach.contains() >= stomach_capacity / 8 && get_hunger() > -1 ) { // that's really all the food you need to feel full set_hunger( -1 ); } else if( stomach.contains() == 0_ml ) { - if( guts.get_calories() == 0 && guts.get_calories_absorbed() == 0 && - get_stored_kcal() < get_healthy_kcal() && get_hunger() < 300 ) { + if( guts.get_calories() == 0 && get_stored_kcal() < get_healthy_kcal() && get_hunger() < 300 ) { // there's no food except what you have stored in fat set_hunger( 300 ); } else if( get_hunger() < 100 && ( ( guts.get_calories() == 0 && - guts.get_calories_absorbed() == 0 && get_stored_kcal() >= get_healthy_kcal() ) || get_stored_kcal() < get_healthy_kcal() ) ) { set_hunger( 100 ); } else if( get_hunger() < 0 ) { @@ -2538,13 +2540,13 @@ void player::update_stomach( const time_point &from, const time_point &to ) // if you just ate but your stomach is still empty it will still // delay your filling up (drugs?) { - if( stomach.contains() >= stomach.capacity() && get_hunger() > -61 ) { + if( stomach.contains() >= stomach_capacity && get_hunger() > -61 ) { // you're engorged! your stomach is full to bursting! set_hunger( -61 ); - } else if( stomach.contains() >= stomach.capacity() * 3 / 4 && get_hunger() > -21 ) { + } else if( stomach.contains() >= stomach_capacity * 3 / 4 && get_hunger() > -21 ) { // sated set_hunger( -21 ); - } else if( stomach.contains() >= stomach.capacity() / 2 && get_hunger() > -1 ) { + } else if( stomach.contains() >= stomach_capacity / 2 && get_hunger() > -1 ) { // that's really all the food you need to feel full set_hunger( -1 ); } else if( stomach.contains() > 0_ml && get_kcal_percent() > 0.95 ) { @@ -2642,7 +2644,7 @@ void player::check_needs_extremes() } else { if( calendar::once_every( 1_hours ) ) { std::string message; - if( stomach.contains() <= stomach.capacity() / 4 ) { + if( stomach.contains() <= stomach.capacity( *this ) / 4 ) { if( get_kcal_percent() < 0.1f ) { message = _( "Food…" ); } else if( get_kcal_percent() < 0.25f ) { @@ -2798,9 +2800,9 @@ void player::check_needs_extremes() } -needs_rates player::calc_needs_rates() +needs_rates player::calc_needs_rates() const { - effect &sleep = get_effect( effect_sleep ); + const effect &sleep = get_effect( effect_sleep ); const bool has_recycler = has_bionic( bio_recycler ); const bool asleep = !sleep.is_null(); @@ -9426,7 +9428,7 @@ std::pair player::get_hunger_description() const { const bool calorie_deficit = get_bmi() < character_weight_category::normal; const units::volume contains = stomach.contains(); - const units::volume cap = stomach.capacity(); + const units::volume cap = stomach.capacity( *this ); std::string hunger_string; nc_color hunger_color = c_white; // i ate just now! diff --git a/src/player.h b/src/player.h index b048241aa1fad..5d0f2949fccc0 100644 --- a/src/player.h +++ b/src/player.h @@ -237,7 +237,7 @@ class player : public Character 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(); + needs_rates calc_needs_rates() const; /** * Handles passive regeneration of pain and maybe hp. diff --git a/src/stomach.cpp b/src/stomach.cpp index da3c8598f8c81..063945766647c 100644 --- a/src/stomach.cpp +++ b/src/stomach.cpp @@ -1,29 +1,19 @@ -#include -#include #include -#include #include "avatar.h" -#include "calendar.h" #include "cata_utility.h" #include "json.h" -#include "player.h" #include "stomach.h" #include "units.h" -#include "compatibility.h" #include "game.h" -#include "item.h" #include "itype.h" -#include "optional.h" -#include "rng.h" -#include "character.h" -#include "options.h" stomach_contents::stomach_contents() = default; -stomach_contents::stomach_contents( units::volume max_vol ) +stomach_contents::stomach_contents( units::volume max_vol, bool is_stomach ) { max_volume = max_vol; + stomach = is_stomach; last_ate = calendar::before_time_starts; } @@ -36,7 +26,6 @@ void stomach_contents::serialize( JsonOut &json ) const { json.start_object(); json.member( "vitamins", vitamins ); - json.member( "vitamins_absorbed", vitamins_absorbed ); json.member( "calories", calories ); json.member( "water", ml_to_string( water ) ); json.member( "max_volume", ml_to_string( max_volume ) ); @@ -54,7 +43,6 @@ void stomach_contents::deserialize( JsonIn &json ) { JsonObject jo = json.get_object(); jo.read( "vitamins", vitamins ); - jo.read( "vitamins_absorbed", vitamins_absorbed ); jo.read( "calories", calories ); std::string str; jo.read( "water", str ); @@ -66,27 +54,27 @@ void stomach_contents::deserialize( JsonIn &json ) jo.read( "last_ate", last_ate ); } -units::volume stomach_contents::capacity() const +units::volume stomach_contents::capacity( const Character &owner ) const { float max_mod = 1; - if( g->u.has_trait( trait_id( "GIZZARD" ) ) ) { + if( owner.has_trait( trait_id( "GIZZARD" ) ) ) { max_mod *= 0.9; } - if( g->u.has_active_mutation( trait_id( "HIBERNATE" ) ) ) { + if( owner.has_active_mutation( trait_id( "HIBERNATE" ) ) ) { max_mod *= 3; } - if( g->u.has_active_mutation( trait_id( "GOURMAND" ) ) ) { + if( owner.has_active_mutation( trait_id( "GOURMAND" ) ) ) { max_mod *= 2; } - if( g->u.has_trait( trait_id( "SLIMESPAWNER" ) ) ) { + if( owner.has_trait( trait_id( "SLIMESPAWNER" ) ) ) { max_mod *= 3; } return max_volume * max_mod; } -units::volume stomach_contents::stomach_remaining() const +units::volume stomach_contents::stomach_remaining( const Character &owner ) const { - return capacity() - contents - water; + return capacity( owner ) - contents - water; } units::volume stomach_contents::contains() const @@ -94,281 +82,92 @@ units::volume stomach_contents::contains() const return contents + water; } -bool stomach_contents::store_absorbed( player &p ) +void stomach_contents::ingest( const nutrients &ingested ) { - if( p.is_npc() && get_option( "NO_NPC_FOOD" ) ) { - return false; + contents += ingested.solids; + water += ingested.water; + calories += ingested.kcal; + for( const std::pair &vit : ingested.vitamins ) { + vitamins[vit.first] += vit.second; } - bool absorbed = false; - if( calories_absorbed != 0 ) { - p.mod_stored_kcal( calories_absorbed ); - absorbed = true; - } - p.vitamins_mod( vitamins_absorbed, false ); - vitamins_absorbed.clear(); - return absorbed; -} - -void stomach_contents::bowel_movement( const stomach_pass_rates &rates ) -{ - int cal_rate = static_cast( round( calories * rates.percent_kcal ) ); - cal_rate = clamp( cal_rate, std::min( rates.min_kcal, calories - calories_absorbed ), - calories - calories_absorbed ); - if( cal_rate < 0 ) { - cal_rate = 0; - } - mod_calories( -cal_rate ); - calories_absorbed = 0; - units::volume contents_rate = clamp( units::from_milliliter( round( - units::to_milliliter - ( contents ) * rates.percent_vol ) ), rates.min_vol, contents ); - mod_contents( -contents_rate ); - // water is a special case. - water = 0_ml; - - for( auto &vit : vitamins ) { - const int vit_absorbed = vitamins_absorbed[vit.first]; - // need to take absorbed vitamins into account for % moved - int percent = static_cast( ( vit.second + vit_absorbed ) * rates.percent_vit ); - // minimum vitamins that will move - int min = rates.min_vit; - int calc = std::max( percent, min ); - - if( vit_absorbed >= calc ) { - // vitamins are absorbed instead of moved - // assumed absorption is called from another function - calc = 0; - } else if( vit_absorbed != 0 ) { - calc -= vit_absorbed; - } - - if( calc > vitamins[vit.first] ) { - calc = vitamins[vit.first]; - } - - vitamins[vit.first] -= calc; - } -} - -void stomach_contents::bowel_movement() -{ - stomach_pass_rates rates; - - /** - * The min numbers aren't technically needed since the percentage will - * take care of the entirety, but some values needed to be assigned - */ - rates.percent_kcal = 1; - rates.min_kcal = 250; - rates.percent_vol = 1; - rates.min_vol = 25000_ml; - rates.percent_vit = 1; - rates.min_vit = 100; - - bowel_movement( rates ); + ate(); } -void stomach_contents::bowel_movement( const stomach_pass_rates &rates, stomach_contents &move_to ) +nutrients stomach_contents::digest( const Character &owner, const needs_rates &metabolic_rates, + int five_mins, int half_hours ) { - int cal_rate = static_cast( round( calories * rates.percent_kcal ) ); - cal_rate = clamp( cal_rate, std::min( rates.min_kcal, calories - calories_absorbed ), - calories - calories_absorbed ); - move_to.mod_calories( cal_rate ); - units::volume contents_rate = clamp( units::from_milliliter( round( - units::to_milliliter - ( contents ) * rates.percent_vol ) ), rates.min_vol, contents ); - move_to.mod_contents( -contents_rate ); - move_to.water += water; + nutrients digested; + stomach_digest_rates rates = get_digest_rates( metabolic_rates, owner ); - for( auto &vit : vitamins ) { - const int vit_absorbed = vitamins_absorbed[vit.first]; - // need to take absorbed vitamins into account for % moved - int percent = static_cast( ( vit.second + vit_absorbed ) * rates.percent_vit ); - // minimum vitamins that will move - int min = rates.min_vit; - int calc = std::max( percent, min ); + // Digest water, but no more than in stomach. + digested.water = std::min( water, rates.water * five_mins ); + water -= digested.water; - if( vit_absorbed >= calc ) { - // vitamins are absorbed instead of moved - // assumed absorption is called from another function - calc = 0; - } else if( vit_absorbed != 0 ) { - calc -= vit_absorbed; + // If no half-hour intervals have passed, we only process water, so bail out early. + if( half_hours == 0 ) { + // We need to initialize these to zero first, though. + digested.kcal = 0; + digested.solids = 0_ml; + for( const std::pair &vit : digested.vitamins ) { + digested.vitamins[vit.first] = 0; } - - if( calc > vitamins[vit.first] ) { - calc = vitamins[vit.first]; - } - - move_to.vitamins[vit.first] += calc; + return digested; } - bowel_movement( rates ); -} -void stomach_contents::ingest( player &p, item &food, int charges = 1 ) -{ - item comest; - if( food.is_container() ) { - comest = food.get_contained(); - } else { - comest = food; - } - if( charges == 0 ) { - // if charges is 0, it means the item is not count_by_charges - charges = 1; - } - // maybe move tapeworm to digestion - for( const auto &v : p.vitamins_from( comest ) ) { - vitamins[v.first] += p.has_effect( efftype_id( "tapeworm" ) ) ? v.second / 2 : v.second; - } - auto &comest_t = comest.type->comestible; - const units::volume add_water = std::max( 0_ml, comest_t->quench * 5_ml ); - // if this number is negative, we decrease the quench/increase the thirst of the player - if( comest_t->quench < 0 ) { - p.mod_thirst( -comest_t->quench ); - } else { - mod_quench( comest_t->quench ); - } - // @TODO: Move quench values to mL and remove the magic number here - mod_contents( comest.base_volume() * charges - add_water ); - - ate(); - - mod_calories( p.kcal_for( comest ) ); -} - -void stomach_contents::absorb_water( player &p, units::volume amount ) -{ - if( water < amount ) { - amount = water; - water = 0_ml; - } else { - water -= amount; - } - if( p.get_thirst() < -100 ) { - return; - } else if( p.get_thirst() - units::to_milliliter( amount / 5 ) < -100 ) { - p.set_thirst( -100 ); - } - p.mod_thirst( -units::to_milliliter( amount ) / 5 ); -} + // Digest solids, but no more than in stomach. + digested.solids = std::min( contents, rates.solids * half_hours ); + contents -= digested.solids; -void stomach_contents::absorb_kcal( int amount ) -{ - if( amount <= 0 ) { - return; - } - calories_absorbed += amount; - calories -= amount; - if( calories < 0 ) { // just a little trickery to avoid overflow - calories_absorbed += calories; - calories = 0; - } -} + // Digest kCal -- use min_kcal by default, but no more than what's in stomach, + // and no less than percentage_kcal of what's in stomach. + digested.kcal = half_hours * clamp( rates.min_kcal, + static_cast( round( calories * rates.percent_kcal ) ), calories ); + calories -= digested.kcal; -bool stomach_contents::absorb_vitamin( const vitamin_id &vit, int amount ) -{ - if( amount <= 0 ) { - return false; + // Digest vitamins just like we did kCal, but we need to do one at a time. + for( const std::pair &vit : vitamins ) { + digested.vitamins[vit.first] = half_hours * clamp( rates.min_vitamin, + static_cast( round( vit.second * rates.percent_vitamin ) ), vit.second ); + vitamins[vit.first] -= digested.vitamins[vit.first]; } - vitamins_absorbed[vit] += amount; - vitamins[vit] -= amount; - if( vitamins[vit] < 0 ) { - vitamins_absorbed[vit] += vitamins[vit]; - vitamins[vit] = amount; - } - return true; -} -bool stomach_contents::absorb_vitamin( const std::pair &vit ) -{ - return absorb_vitamin( vit.first, vit.second ); + return digested; } -bool stomach_contents::absorb_vitamins( const std::map &vitamins ) +void stomach_contents::empty() { - bool absorbed = false; - for( const std::pair vit : vitamins ) { - if( absorb_vitamin( vit ) ) { - absorbed = true; - } - } - return absorbed; + calories = 0; + water = 0_ml; + contents = 0_ml; + vitamins.clear(); } -stomach_pass_rates stomach_contents::get_pass_rates( bool stomach ) +stomach_digest_rates stomach_contents::get_digest_rates( const needs_rates &metabolic_rates, + const Character &owner ) { - stomach_pass_rates rates; - // a human will digest food in about 53 hours - // most of the excrement is completely indigestible - // ie almost all of the calories ingested are absorbed. - // 3 hours will be accounted here as stomach - // the rest will be guts + stomach_digest_rates rates; if( stomach ) { - rates.min_vol = capacity() / 6; + // The stomach is focused on passing material on to the guts. // 3 hours to empty in 30 minute increments - rates.percent_vol = 1.0f / 6.0f; - rates.min_vit = 1; - rates.percent_vit = 1.0f / 6.0f; + rates.solids = capacity( owner ) / 6; + rates.water = 250_ml; // Water is special, passes very quickly, in 5 minute intervals + rates.min_vitamin = 1; + rates.percent_vitamin = 1.0f / 6.0f; rates.min_kcal = 5; rates.percent_kcal = 1.0f / 6.0f; } else { - rates.min_vol = std::max( capacity() / 100, 1_ml ); - // 50 hours to empty in 30 minute increments - rates.percent_vol = 0.01f; - rates.min_vit = 1; - rates.percent_vit = 0.01f; - rates.min_kcal = 5; - rates.percent_kcal = 0.01f; - } - return rates; -} - -stomach_absorb_rates stomach_contents::get_absorb_rates( bool stomach, - const needs_rates &metabolic_rates ) -{ - stomach_absorb_rates rates; - if( !stomach ) { + // The guts are focused on absorption into the body, we don't care about passing rates. + // Solids rate doesn't do anything impactful here so just make it big enough to avoid overflow. + rates.solids = 250_ml; + rates.water = 250_ml; rates.min_kcal = roll_remainder( metabolic_rates.kcal / 24.0 * metabolic_rates.hunger ); rates.percent_kcal = 0.05f * metabolic_rates.hunger; - rates.min_vitamin_default = round( 100.0 / 24.0 * metabolic_rates.hunger ); - rates.percent_vitamin_default = 0.05f * metabolic_rates.hunger; - } else { - rates.min_kcal = 0; - rates.percent_kcal = 0.0f; - rates.min_vitamin_default = 0; - rates.percent_vitamin_default = 0.0f; + rates.min_vitamin = round( 100.0 / 24.0 * metabolic_rates.hunger ); + rates.percent_vitamin = 0.05f * metabolic_rates.hunger; } return rates; } -void stomach_contents::calculate_absorbed( stomach_absorb_rates rates ) -{ - for( const auto vit : vitamins ) { - int min_v; - float percent_v; - if( rates.min_vitamin.find( vit.first ) != rates.min_vitamin.end() ) { - min_v = rates.min_vitamin[vit.first]; - } else { - min_v = rates.min_vitamin_default; - } - if( rates.percent_vitamin.find( vit.first ) != rates.percent_vitamin.end() ) { - percent_v = rates.percent_vitamin[vit.first]; - } else { - percent_v = rates.percent_vitamin_default; - } - int vit_absorbed = std::max( min_v, static_cast( percent_v * vit.second ) ); - // make sure the vitamins don't overflow - vit_absorbed = std::min( vit_absorbed, vit.second ); - vitamins_absorbed[vit.first] += vit_absorbed; - vitamins[vit.first] -= vit_absorbed; - } - int cal = clamp( static_cast( round( calories * rates.percent_kcal ) ), - std::min( rates.min_kcal, calories ), calories ); - calories_absorbed += cal; - calories -= cal; -} - void stomach_contents::mod_calories( int cal ) { if( -cal >= calories ) { @@ -378,11 +177,6 @@ void stomach_contents::mod_calories( int cal ) calories += cal; } -void stomach_contents::set_calories( int cal ) -{ - calories = cal; -} - void stomach_contents::mod_nutr( int nutr ) { // nutr is legacy type code, this function simply converts old nutrition to new kcal @@ -415,16 +209,6 @@ int stomach_contents::get_calories() const return calories; } -int stomach_contents::get_calories_absorbed() const -{ - return calories_absorbed; -} - -void stomach_contents::set_calories_absorbed( int cal ) -{ - calories_absorbed = cal; -} - units::volume stomach_contents::get_water() const { return water; @@ -443,9 +227,9 @@ time_duration stomach_contents::time_since_ate() const // sets default stomach contents when starting the game void Character::initialize_stomach_contents() { - stomach = stomach_contents( 2500_ml ); - guts = stomach_contents( 24000_ml ); - guts.set_calories( 300 ); - stomach.set_calories( 800 ); + stomach = stomach_contents( 2500_ml, true ); + guts = stomach_contents( 24000_ml, false ); + guts.mod_calories( 300 ); + stomach.mod_calories( 800 ); stomach.mod_contents( 475_ml ); } diff --git a/src/stomach.h b/src/stomach.h index 28f975400f217..21a27ecddc26d 100644 --- a/src/stomach.h +++ b/src/stomach.h @@ -1,38 +1,31 @@ #pragma once #include -#include #include "units.h" -#include "calendar.h" -#include "type_id.h" struct needs_rates; -class player; class JsonIn; class JsonOut; class item; -// 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; +// Contains all information that can pass out of (or into) a stomach +struct nutrients { + units::volume water; + units::volume solids; + int kcal; + std::map vitamins; }; -// how much a stomach_contents can absorb +// how much a stomach_contents can digest // based on 30 minute increments -struct stomach_absorb_rates { +struct stomach_digest_rates { + units::volume solids; + units::volume water; float percent_kcal; int min_kcal; - std::map percent_vitamin; - std::map min_vitamin; - float percent_vitamin_default; - int min_vitamin_default; + float percent_vitamin; + int min_vitamin; }; // an abstract of food that has been eaten. @@ -40,46 +33,60 @@ 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( const stomach_pass_rates &rates ); - // moves @rates contents to other stomach_contents - // amount ranges from 0 to 1 - void bowel_movement( const 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; + /** + * @brief Construct a new stomach contents object + * Stomachs always process their food in a few hours. Guts take significantly longer, + * and their rate is dependent on the metabolic rate of their owner. + * @param max_volume Base capacity, subject to modification, i.e. by traits + * @param is_stomach If true, treated as stomach, if false, treated as guts + */ + stomach_contents( units::volume max_volume, bool is_stomach ); + + /** + * @brief Directly adds nutrients to stomach contents. + * Will still add contents if past maximum volume. Also updates last_ate to current turn. + * @param ingested The nutrients to be ingested + */ + void ingest( const nutrients &ingested ); + + /** + * @brief Processes food and outputs nutrients that are finished processing + * Metabolic rates are required because they determine the rate of absorption of + * nutrients into the body. + * All returned values are >= 0, with the exception of water, which + * can be negative in some circumstances (i.e. after eating dry/salty food). + * TODO: Update stomach capacity upon mutation changes, instead of calculating every time we + * need it, so we can get rid of 'owner' parameter here. + * @param owner The owner of this stomach + * @param metabolic_rates The metabolic rates of the owner of this stomach + * @param five_mins Five-minute intervals passed since this method was last called + * @param half_hours Half-hour intervals passed since this method was last called + * @return nutrients that are done processing in this stomach + */ + nutrients digest( const Character &owner, const needs_rates &metabolic_rates, + int five_mins, int half_hours ); + + // Empties the stomach of all contents. + void empty(); + + /** + * @brief Calculates the capacity of this stomach. + * This function needs a ref to the stomach's owner so it can account for relevant mutations. + * TODO: JSONize stomach capacity multipliers. + * @param owner This stomach's owner + * @return This stomach's capacity, in units::volume + */ + units::volume capacity( const Character &owner ) const; // how much stomach capacity you have left before you puke from stuffing your gob - units::volume stomach_remaining() const; + units::volume stomach_remaining( const Character &owner ) 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, const 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 ); @@ -92,13 +99,6 @@ class stomach_contents // 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; @@ -110,14 +110,13 @@ class stomach_contents private: + // If true, this object represents a stomach; if false, this object represents guts. + bool stomach; + // vitamins in stomach_contents std::map vitamins; - // vitamins to be absorbed in stomach_contents - std::map 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; /** @@ -131,21 +130,8 @@ class stomach_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( const 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( const std::pair &vit ); - // absorbs multiple vitamins - // does not add it to player vitamins yet - // returns true if any vitamins are absorbed - bool absorb_vitamins( const std::map &vitamins ); + // Gets the rates at which this stomach will digest things. + stomach_digest_rates get_digest_rates( const needs_rates &metabolic_rates, + const Character &owner ); }; diff --git a/tests/stomach_contents_tests.cpp b/tests/stomach_contents_tests.cpp index e53662e01e427..dee3711b14cd1 100644 --- a/tests/stomach_contents_tests.cpp +++ b/tests/stomach_contents_tests.cpp @@ -31,10 +31,8 @@ static void pass_time( player &p, time_duration amt ) static void clear_stomach( player &p ) { - p.guts.set_calories( 0 ); - p.stomach.set_calories( 0 ); - p.stomach.bowel_movement(); - p.guts.bowel_movement(); + p.stomach.empty(); + p.guts.empty(); } static void set_all_vitamins( int target, player &p ) @@ -63,14 +61,13 @@ static 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 guts: %d player: %d/%d hunger: %d\n", p.stomach.get_calories(), + p.guts.get_calories(), 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( p.stomach.contains() ), - units::to_milliliter( p.stomach.capacity() ), + units::to_milliliter( p.stomach.capacity( p ) ), units::to_milliliter( p.guts.contains() ), - units::to_milliliter( p.guts.capacity() ) ); + units::to_milliliter( p.guts.capacity( p ) ) ); printf( "metabolic rate: %.2f\n", p.metabolic_rate() ); } @@ -78,24 +75,9 @@ static void print_stomach_contents( player &p, const bool print ) // accounting for appropriate vitamins static void eat_all_nutrients( player &p ) { - // Absorption rates are imperfect for now, so target is 140% DV (== 135 vitamin units if rate is 15m) - item f( "fried_brain" ); - p.eat( f ); - f = item( "carrot" ); - p.eat( f ); - f = item( "carrot" ); - p.eat( f ); - f = item( "hotdogs_cooked" ); - p.eat( f ); - f = item( "veggy" ); - p.eat( f ); - f = item( "can_herring" ); - p.eat( f ); - f = item( "can_herring" ); - p.eat( f ); - f = item( "can_herring" ); - p.eat( f ); - f = item( "junk_burrito" ); + // Vitamin target: 100% DV -- or 96 vitamin "units" since all vitamins currently decay every 15m. + // Energy target: 2100 kcal -- debug target will be completely sedentary. + item f( "debug_nutrition" ); p.eat( f ); }