From 927e94da7ca80ce3cd858ccb9dd25aceaf4ad2ba Mon Sep 17 00:00:00 2001 From: DaviBones Date: Sun, 10 Nov 2019 09:26:45 -0500 Subject: [PATCH] Stomach code refactor (#35143) * Remove unused methods from stomach.cpp * Combine several methods so stomach.cpp is simpler There were several issues with the previous implementation of the stomach_contents class. For instance, using the class required intricate knowledge of its implementation. Additionally, the most important and commonly performed task, digesting food every half-hour, required calling four specific methods in a specific order, when they could easily be combined into one method. Overall, the class suffered from over-complexity and I decided to fix this by making the following changes: Combined store_water, calculate_absorbed, store_absorbed, and bowel_movement into one method, "digest". Removed references to calories_absorbed and vitamins_absorbed, as these are no longer used after the above change. Combined pass_rates and absorb_rates: stomach-types never absorbed anything, and guts-types passed so slowly as to be inconsequential, so these concepts were combined into digest_rates. Whether a stomach_contents is a stomach or guts is now decided when the object is constructed; it was already that way in practice, I just made it official. * Make all_nutrition_starve_test far more strict Now that vitamins aren't disappearing, this test can be tuned to make sure that vitamins are being absorbed into the body correctly. * Implement suggestions from KorGgenT - Replace auto with type specification. - Add const where applicable - Replaces 'player' type parameters with 'needs_rates' Co-Authored-By: Curtis Merrill * Remove unused #includes * Refactor ingest function The previous ingest function in stomach.cpp used the 'player' object, which we are moving away from. Since the only call of that function was in the 'player' scope, it was trivial to move that code there and call the new, more generalized ingest function instead. * Fix bug with stomach capacity calculation Stomach capacity was always based on the player's mutations, no matter who owned said stomach. Now the capacity function takes a Character reference as a parameter, and uses that Character's mutations instead. --- data/json/items/comestibles/meat_dishes.json | 10 + src/character.cpp | 3 +- src/consumption.cpp | 23 +- src/debug_menu.cpp | 4 +- src/iuse.cpp | 2 +- src/player.cpp | 50 +-- src/player.h | 2 +- src/stomach.cpp | 352 ++++--------------- src/stomach.h | 140 ++++---- tests/stomach_contents_tests.cpp | 36 +- 10 files changed, 199 insertions(+), 423 deletions(-) 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 2cf2b562f01a9..f2f8b6592c877 100644 --- a/src/character.cpp +++ b/src/character.cpp @@ -5567,8 +5567,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 4a47e425bf562..e6dc3d946db39 100644 --- a/src/consumption.cpp +++ b/src/consumption.cpp @@ -595,7 +595,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 ); @@ -677,10 +677,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() ); @@ -1131,8 +1131,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 3268c99cc0105..7030934ba71e1 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 5329a1dacb537..ec49e0f812acf 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -2388,37 +2388,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 ) { @@ -2435,13 +2437,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 ) { @@ -2539,7 +2541,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 ) { @@ -2695,9 +2697,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(); @@ -7358,7 +7360,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 10aa932ee09c3..947cfda438b47 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 ); }