From b0bffd504ca234b9b036e9f28303a85a4605443e Mon Sep 17 00:00:00 2001 From: anothersimulacrum Date: Thu, 21 Jan 2021 22:45:15 -0800 Subject: [PATCH 1/4] Change assert to require We don't need to crash here, we can just abort the test with REQUIRE --- tests/submap_load_test.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/submap_load_test.cpp b/tests/submap_load_test.cpp index f05c814cd4272..7ad3364e4a9b3 100644 --- a/tests/submap_load_test.cpp +++ b/tests/submap_load_test.cpp @@ -745,7 +745,7 @@ static JsonIn submap_cosmetic( submap_cosmetic_ss ); static void load_from_jsin( submap &sm, JsonIn &jsin ) { // Ensure that the JSON is up to date for our savegame version - cata_assert( savegame_version == 31 ); + REQUIRE( savegame_version == 31 ); jsin.start_object(); int version = 0; while( !jsin.end_object() ) { From 5a522981a648477f718c8faa8d8e47dacee6740b Mon Sep 17 00:00:00 2001 From: anothersimulacrum Date: Thu, 21 Jan 2021 22:08:01 -0800 Subject: [PATCH 2/4] Use calories instead of kcal in nutrients calorie precision is only used internally, all external methods should be dealing with kcal. Having calorie precision just enables removing rng-based rounding from metabolism functions, without losing detail. There was a very minor change in the Calories of some crafted items, as is caught in the tests. --- src/activity_item_handling.cpp | 2 +- src/character.cpp | 2 +- src/consumption.cpp | 16 +++++++----- src/dump.cpp | 2 +- src/faction_camp.cpp | 2 +- src/game_inventory.cpp | 2 +- src/item.cpp | 10 ++++---- src/item_factory.cpp | 13 +++++++--- src/itype.h | 4 +-- src/savegame.cpp | 2 +- src/stomach.cpp | 47 +++++++++++++++++++++------------- src/stomach.h | 10 +++++--- tests/comestible_test.cpp | 18 ++++++------- tests/iteminfo_test.cpp | 2 +- tests/submap_load_test.cpp | 28 ++++++++++---------- 15 files changed, 90 insertions(+), 70 deletions(-) diff --git a/src/activity_item_handling.cpp b/src/activity_item_handling.cpp index e95afe49576f2..4401c3037a6c6 100644 --- a/src/activity_item_handling.cpp +++ b/src/activity_item_handling.cpp @@ -2905,7 +2905,7 @@ int get_auto_consume_moves( player &p, const bool food ) if( !p.can_consume( comest ) ) { continue; } - if( food && p.compute_effective_nutrients( comest ).kcal < 50 ) { + if( food && p.compute_effective_nutrients( comest ).kcal() < 50 ) { // not filling enough continue; } diff --git a/src/character.cpp b/src/character.cpp index 9cb135e725159..3e00e0dc17958 100644 --- a/src/character.cpp +++ b/src/character.cpp @@ -5702,7 +5702,7 @@ void Character::update_stomach( const time_point &from, const time_point &to ) guts.ingest( digested_to_guts ); // Apply nutrients, unless this is an NPC and NO_NPC_FOOD is enabled. if( !npc_no_food ) { - mod_stored_kcal( digested_to_body.nutr.kcal ); + mod_stored_kcal( digested_to_body.nutr.kcal() ); log_activity_level( activity_level() ); vitamins_mod( digested_to_body.nutr.vitamins, false ); } diff --git a/src/consumption.cpp b/src/consumption.cpp index cd09169aa64c7..ade528ab09622 100644 --- a/src/consumption.cpp +++ b/src/consumption.cpp @@ -152,7 +152,7 @@ static int compute_default_effective_kcal( const item &comest, const Character & } // As float to avoid rounding too many times - float kcal = comest.get_comestible()->default_nutrition.kcal; + float kcal = comest.get_comestible()->default_nutrition.kcal(); // Many raw foods give less calories, as your body has expends more energy digesting them. bool cooked = comest.has_flag( flag_COOKED ) || extra_flags.count( flag_COOKED ); @@ -231,7 +231,8 @@ static std::map compute_default_effective_vitamins( static nutrients compute_default_effective_nutrients( const item &comest, const Character &you, const cata::flat_set &extra_flags = {} ) { - return { compute_default_effective_kcal( comest, you, extra_flags ), + // Multiply by 1000 to get it in calories + return { compute_default_effective_kcal( comest, you, extra_flags ) * 1000, compute_default_effective_vitamins( comest, you ) }; } @@ -373,7 +374,7 @@ std::pair Character::compute_nutrient_range( int Character::nutrition_for( const item &comest ) const { - return compute_effective_nutrients( comest ).kcal / islot_comestible::kcal_per_nutr; + return compute_effective_nutrients( comest ).kcal() / islot_comestible::kcal_per_nutr; } std::pair Character::fun_for( const item &comest ) const @@ -1228,7 +1229,8 @@ double Character::compute_effective_food_volume_ratio( const item &food ) const units::mass food_weight = ( food.weight() / food.count() ); double ratio = 1.0f; if( units::to_gram( food_weight ) != 0 ) { - ratio = std::max( static_cast( food_nutrients.kcal ) / units::to_gram( food_weight ), 1.0 ); + ratio = std::max( static_cast( food_nutrients.kcal() ) / units::to_gram( food_weight ), + 1.0 ); if( ratio > 3.0f ) { ratio = std::sqrt( 3 * ratio ); } @@ -1264,9 +1266,9 @@ int Character::compute_calories_per_effective_volume( const item &food, int kcalories; if( nutrient ) { // if given the optional nutrient argument, we will compute kcal based on that. ( Crafting menu ). - kcalories = nutrient->kcal; + kcalories = nutrient->kcal(); } else { - kcalories = compute_effective_nutrients( food ).kcal; + kcalories = compute_effective_nutrients( food ).kcal(); } units::volume food_vol = masticated_volume( food ) * food.count(); // Divide by 1000 to convert to L. Final quantity is essentially dimensionless, so unit of measurement does not matter. @@ -1394,7 +1396,7 @@ bool Character::consume_effects( item &food ) add_msg_debug( "Effective volume: %d (solid) %d (liquid)\n multiplier: %g calories: %d, weight: %d", units::to_milliliter( ingested.solids ), units::to_milliliter( ingested.water ), ratio, - food_nutrients.kcal, units::to_gram( food_weight ) ); + food_nutrients.kcal(), units::to_gram( food_weight ) ); // Maybe move tapeworm to digestion if( has_effect( effect_tapeworm ) ) { ingested.nutr /= 2; diff --git a/src/dump.cpp b/src/dump.cpp index fe35a094e57b3..380ed8a042b33 100644 --- a/src/dump.cpp +++ b/src/dump.cpp @@ -141,7 +141,7 @@ bool game::dump_stats( const std::string &what, dump_mode mode, r.push_back( to_string( obj.volume() / units::legacy_volume_factor ) ); r.push_back( to_string( to_gram( obj.weight() ) ) ); r.push_back( to_string( obj.type->stack_size ) ); - r.push_back( to_string( obj.get_comestible()->default_nutrition.kcal ) ); + r.push_back( to_string( obj.get_comestible()->default_nutrition.kcal() ) ); r.push_back( to_string( obj.get_comestible()->quench ) ); r.push_back( to_string( obj.get_comestible()->healthy ) ); auto vits = obj.get_comestible()->default_nutrition.vitamins; diff --git a/src/faction_camp.cpp b/src/faction_camp.cpp index 64732dd009ca6..a7d321d958e20 100644 --- a/src/faction_camp.cpp +++ b/src/faction_camp.cpp @@ -3821,7 +3821,7 @@ bool basecamp::distribute_food() if( it.rotten() ) { return false; } - const int kcal = it.get_comestible()->default_nutrition.kcal * it.count() * rot_multip( it, + const int kcal = it.get_comestible()->default_nutrition.kcal() * it.count() * rot_multip( it, container ); if( kcal <= 0 ) { // can happen if calories is low and rot is high. diff --git a/src/game_inventory.cpp b/src/game_inventory.cpp index 3cadff0c5b6cc..c4369976a0cd5 100644 --- a/src/game_inventory.cpp +++ b/src/game_inventory.cpp @@ -526,7 +526,7 @@ class comestible_inventory_preset : public inventory_selector_preset append_cell( [&p]( const item_location & loc ) { const nutrients nutr = p.compute_effective_nutrients( *loc ); - return good_bad_none( nutr.kcal ); + return good_bad_none( nutr.kcal() ); }, _( "CALORIES" ) ); append_cell( []( const item_location & loc ) { diff --git a/src/item.cpp b/src/item.cpp index 14ba31eba178e..6ba5673f3cf34 100644 --- a/src/item.cpp +++ b/src/item.cpp @@ -1845,13 +1845,13 @@ void item::food_info( const item *food_item, std::vector &info, } } - if( max_nutr.kcal != 0 || food_item->get_comestible()->quench != 0 ) { + if( max_nutr.kcal() != 0 || food_item->get_comestible()->quench != 0 ) { if( parts->test( iteminfo_parts::FOOD_NUTRITION ) ) { info.push_back( iteminfo( "FOOD", _( "Calories (kcal): " ), - "", iteminfo::no_newline, min_nutr.kcal ) ); - if( max_nutr.kcal != min_nutr.kcal ) { + "", iteminfo::no_newline, min_nutr.kcal() ) ); + if( max_nutr.kcal() != min_nutr.kcal() ) { info.push_back( iteminfo( "FOOD", _( "-" ), - "", iteminfo::no_newline, max_nutr.kcal ) ); + "", iteminfo::no_newline, max_nutr.kcal() ) ); } } if( parts->test( iteminfo_parts::FOOD_QUENCH ) ) { @@ -1861,7 +1861,7 @@ void item::food_info( const item *food_item, std::vector &info, } if( parts->test( iteminfo_parts::FOOD_SATIATION ) ) { - if( max_nutr.kcal == min_nutr.kcal ) { + if( max_nutr.kcal() == min_nutr.kcal() ) { info.push_back( iteminfo( "FOOD", _( "Satiety: " ), satiety_bar( player_character.compute_calories_per_effective_volume( *food_item ) ) ) ); } else { diff --git a/src/item_factory.cpp b/src/item_factory.cpp index 8e4d10b7a471f..8def4dfd10ec2 100644 --- a/src/item_factory.cpp +++ b/src/item_factory.cpp @@ -2263,19 +2263,24 @@ void Item_factory::load( islot_comestible &slot, const JsonObject &jo, const std bool got_calories = false; if( jo.has_member( "calories" ) ) { - slot.default_nutrition.kcal = jo.get_int( "calories" ); + // The value here is in kcal, but is stored as simply calories + slot.default_nutrition.calories = 1000 * jo.get_int( "calories" ); got_calories = true; } else if( relative.has_member( "calories" ) ) { - slot.default_nutrition.kcal += relative.get_int( "calories" ); + // The value here is in kcal, but is stored as simply calories + slot.default_nutrition.calories += 1000 * relative.get_int( "calories" ); got_calories = true; } else if( proportional.has_member( "calories" ) ) { - slot.default_nutrition.kcal *= proportional.get_float( "calories" ); + // The value here is in kcal, but is stored as simply calories + slot.default_nutrition.calories *= proportional.get_float( "calories" ); got_calories = true; } else if( jo.has_member( "nutrition" ) ) { - slot.default_nutrition.kcal = jo.get_int( "nutrition" ) * islot_comestible::kcal_per_nutr; + // The value here is in kcal, but is stored as simply calories + slot.default_nutrition.calories = jo.get_int( "nutrition" ) * islot_comestible::kcal_per_nutr * + 1000; } if( jo.has_member( "nutrition" ) && got_calories ) { diff --git a/src/itype.h b/src/itype.h index c3deba329883b..5632ac4d81db3 100644 --- a/src/itype.h +++ b/src/itype.h @@ -181,11 +181,11 @@ struct islot_comestible { static constexpr float kcal_per_nutr = 2500.0f / ( 12 * 24 ); bool has_calories() const { - return default_nutrition.kcal > 0; + return default_nutrition.calories > 0; } int get_default_nutr() const { - return default_nutrition.kcal / kcal_per_nutr; + return default_nutrition.kcal() / kcal_per_nutr; } /** The monster group that is drawn from when the item rots away */ diff --git a/src/savegame.cpp b/src/savegame.cpp index 24b2257061f37..3aefd0f65c2a5 100644 --- a/src/savegame.cpp +++ b/src/savegame.cpp @@ -53,7 +53,7 @@ extern std::map> quick_shortcuts_map; * Changes that break backwards compatibility should bump this number, so the game can * load a legacy format loader. */ -const int savegame_version = 31; +const int savegame_version = 32; /* * This is a global set by detected version header in .sav, maps.txt, or overmap. diff --git a/src/stomach.cpp b/src/stomach.cpp index e8a9794a2eab4..85500a2699a39 100644 --- a/src/stomach.cpp +++ b/src/stomach.cpp @@ -6,6 +6,7 @@ #include "cata_utility.h" #include "character.h" #include "compatibility.h" +#include "game.h" #include "json.h" #include "player.h" #include "rng.h" @@ -15,7 +16,7 @@ void nutrients::min_in_place( const nutrients &r ) { - kcal = std::min( kcal, r.kcal ); + calories = std::min( calories, r.calories ); for( const std::pair &vit_pair : vitamin::all() ) { const vitamin_id &vit = vit_pair.first; int other = r.get_vitamin( vit ); @@ -32,7 +33,7 @@ void nutrients::min_in_place( const nutrients &r ) void nutrients::max_in_place( const nutrients &r ) { - kcal = std::max( kcal, r.kcal ); + calories = std::max( calories, r.calories ); for( const std::pair &vit_pair : vitamin::all() ) { const vitamin_id &vit = vit_pair.first; int other = r.get_vitamin( vit ); @@ -52,9 +53,14 @@ int nutrients::get_vitamin( const vitamin_id &vit ) const return it->second; } +int nutrients::kcal() const +{ + return calories / 1000; +} + bool nutrients::operator==( const nutrients &r ) const { - if( kcal != r.kcal ) { + if( kcal() != r.kcal() ) { return false; } // Can't just use vitamins == r.vitamins, because there might be zero @@ -70,7 +76,7 @@ bool nutrients::operator==( const nutrients &r ) const nutrients &nutrients::operator+=( const nutrients &r ) { - kcal += r.kcal; + calories += r.calories; for( const std::pair &vit : r.vitamins ) { vitamins[vit.first] += vit.second; } @@ -79,7 +85,7 @@ nutrients &nutrients::operator+=( const nutrients &r ) nutrients &nutrients::operator-=( const nutrients &r ) { - kcal -= r.kcal; + calories -= r.calories; for( const std::pair &vit : r.vitamins ) { vitamins[vit.first] -= vit.second; } @@ -88,7 +94,7 @@ nutrients &nutrients::operator-=( const nutrients &r ) nutrients &nutrients::operator*=( int r ) { - kcal *= r; + calories *= r; for( std::pair &vit : vitamins ) { vit.second *= r; } @@ -97,7 +103,7 @@ nutrients &nutrients::operator*=( int r ) nutrients &nutrients::operator/=( int r ) { - kcal = divide_round_up( kcal, r ); + calories = divide_round_up( calories, r ); for( std::pair &vit : vitamins ) { vit.second = divide_round_up( vit.second, r ); } @@ -122,7 +128,7 @@ void stomach_contents::serialize( JsonOut &json ) const { json.start_object(); json.member( "vitamins", nutr.vitamins ); - json.member( "calories", nutr.kcal ); + json.member( "calories", nutr.calories ); json.member( "water", ml_to_string( water ) ); json.member( "max_volume", ml_to_string( max_volume ) ); json.member( "contents", ml_to_string( contents ) ); @@ -139,7 +145,11 @@ void stomach_contents::deserialize( JsonIn &json ) { JsonObject jo = json.get_object(); jo.read( "vitamins", nutr.vitamins ); - jo.read( "calories", nutr.kcal ); + jo.read( "calories", nutr.calories ); + // nutr.calories was changed from being in kcal to being in cal + if( savegame_loading_version <= 31 ) { + nutr.calories *= 1000; + } std::string str; jo.read( "water", str ); water = string_to_ml( str ); @@ -194,8 +204,9 @@ food_summary stomach_contents::digest( const Character &owner, const needs_rates // 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. - int kcal_fraction = std::lround( nutr.kcal * rates.percent_kcal ); - digested.nutr.kcal = half_hours * clamp( rates.min_kcal, kcal_fraction, nutr.kcal ); + int kcal_fraction = std::lround( nutr.kcal() * rates.percent_kcal ); + digested.nutr.calories = half_hours * clamp( rates.min_calories, kcal_fraction * 1000, + nutr.calories ); // Digest vitamins just like we did kCal, but we need to do one at a time. for( const std::pair &vit : nutr.vitamins ) { @@ -226,14 +237,14 @@ stomach_digest_rates stomach_contents::get_digest_rates( const needs_rates &meta 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.min_calories = 5000; rates.percent_kcal = 1.0f / 6.0f; } else { // 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.min_calories = roll_remainder( metabolic_rates.kcal / 24.0 * metabolic_rates.hunger ) * 1000; rates.percent_kcal = 0.05f * metabolic_rates.hunger; rates.min_vitamin = std::round( 100.0 / 24.0 * metabolic_rates.hunger ); rates.percent_vitamin = 0.05f * metabolic_rates.hunger; @@ -241,13 +252,13 @@ stomach_digest_rates stomach_contents::get_digest_rates( const needs_rates &meta return rates; } -void stomach_contents::mod_calories( int cal ) +void stomach_contents::mod_calories( int kcal ) { - if( -cal >= nutr.kcal ) { - nutr.kcal = 0; + if( -kcal >= nutr.kcal() ) { + nutr.calories = 0; return; } - nutr.kcal += cal; + nutr.calories += kcal * 1000; } void stomach_contents::mod_nutr( int nutr ) @@ -279,7 +290,7 @@ void stomach_contents::mod_contents( const units::volume &vol ) int stomach_contents::get_calories() const { - return nutr.kcal; + return nutr.kcal(); } units::volume stomach_contents::get_water() const diff --git a/src/stomach.h b/src/stomach.h index f83d1e031a5ff..59a47a0e1398e 100644 --- a/src/stomach.h +++ b/src/stomach.h @@ -17,8 +17,8 @@ struct needs_rates; // Separate struct for nutrients so that we can easily perform arithmetic on // them struct nutrients { - /** amount of kcal this food has */ - int kcal = 0; + /** amount of calories (1/1000s of kcal) this food has */ + int calories = 0; /** vitamins potentially provided by this comestible (if any) */ std::map vitamins; @@ -29,6 +29,7 @@ struct nutrients { void max_in_place( const nutrients &r ); int get_vitamin( const vitamin_id & ) const; + int kcal() const; bool operator==( const nutrients &r ) const; bool operator!=( const nutrients &r ) const { @@ -64,7 +65,8 @@ struct stomach_digest_rates { units::volume solids = 0_ml; units::volume water = 0_ml; float percent_kcal = 0.0f; - int min_kcal = 0; + // calories, or 1/1000s of kcals + int min_calories = 0; float percent_vitamin = 0.0f; int min_vitamin = 0; }; @@ -127,7 +129,7 @@ class stomach_contents units::volume get_water() const; // changes calorie amount - void mod_calories( int calories ); + void mod_calories( int kcal ); // changes calorie amount based on old nutr value void mod_nutr( int nutr ); diff --git a/tests/comestible_test.cpp b/tests/comestible_test.cpp index 5042aa9b09c9a..314c3a1f9e3d9 100644 --- a/tests/comestible_test.cpp +++ b/tests/comestible_test.cpp @@ -29,10 +29,10 @@ static int comp_calories( const std::vector &components ) for( const item_comp &it : components ) { const cata::value_ptr &temp = item::find_type( it.type )->comestible; if( temp && temp->cooks_like.is_empty() ) { - calories += temp->default_nutrition.kcal * it.count; + calories += temp->default_nutrition.kcal() * it.count; } else if( temp ) { const itype *cooks_like = item::find_type( temp->cooks_like ); - calories += cooks_like->comestible->default_nutrition.kcal * it.count; + calories += cooks_like->comestible->default_nutrition.kcal() * it.count; } } return calories; @@ -89,7 +89,7 @@ static int byproduct_calories( const recipe &recipe_obj ) int kcal = 0; for( const item &it : byproducts ) { if( it.is_comestible() ) { - kcal += it.type->comestible->default_nutrition.kcal * it.charges; + kcal += it.type->comestible->default_nutrition.kcal() * it.charges; } } return kcal; @@ -137,7 +137,7 @@ TEST_CASE( "recipe_permutations", "[recipe]" ) // The calories of the result int default_calories = 0; if( res_it.type->comestible ) { - default_calories = res_it.type->comestible->default_nutrition.kcal; + default_calories = res_it.type->comestible->default_nutrition.kcal(); } if( res_it.charges > 0 ) { default_calories *= res_it.charges; @@ -175,8 +175,8 @@ TEST_CASE( "cooked_veggies_get_correct_calorie_prediction", "[recipe]" ) std::pair predicted_nutrition = u.compute_nutrient_range( veggy_wild_cooked, veggy_wild_cooked_recipe ); - CHECK( default_nutrition.kcal == predicted_nutrition.first.kcal ); - CHECK( default_nutrition.kcal == predicted_nutrition.second.kcal ); + CHECK( default_nutrition.kcal() == predicted_nutrition.first.kcal() ); + CHECK( default_nutrition.kcal() == predicted_nutrition.second.kcal() ); } // The Character::compute_effective_food_volume_ratio function returns a floating-point ratio @@ -202,7 +202,7 @@ TEST_CASE( "effective food volume and satiety", "[character][food][satiety]" ) REQUIRE( apple.count() == 1 ); REQUIRE( apple.weight() == 200_gram ); REQUIRE( apple.volume() == 250_ml ); - REQUIRE( apple_nutr.kcal == 95 ); + REQUIRE( apple_nutr.kcal() == 95 ); // If kcal per gram < 1.0, return 1.0 CHECK( u.compute_effective_food_volume_ratio( apple ) == Approx( 1.0f ).margin( 0.01f ) ); CHECK( u.compute_calories_per_effective_volume( apple ) == 500 ); @@ -214,7 +214,7 @@ TEST_CASE( "effective food volume and satiety", "[character][food][satiety]" ) REQUIRE( egg.count() == 1 ); REQUIRE( egg.weight() == 40_gram ); REQUIRE( egg.volume() == 50_ml ); - REQUIRE( egg_nutr.kcal == 80 ); + REQUIRE( egg_nutr.kcal() == 80 ); // If kcal per gram > 1.0 but less than 3.0, return ( kcal / gram ) CHECK( u.compute_effective_food_volume_ratio( egg ) == Approx( 2.0f ).margin( 0.01f ) ); CHECK( u.compute_calories_per_effective_volume( egg ) == 2000 ); @@ -227,7 +227,7 @@ TEST_CASE( "effective food volume and satiety", "[character][food][satiety]" ) REQUIRE( nuts.count() == 4 ); REQUIRE( nuts.weight() == 120_gram ); REQUIRE( nuts.volume() == 250_ml ); - REQUIRE( nuts_nutr.kcal == 202 ); + REQUIRE( nuts_nutr.kcal() == 202 ); // If kcal per gram > 3.0, return sqrt( 3 * kcal / gram ) expect_ratio = std::sqrt( 3.0f * 202 / 30 ); CHECK( u.compute_effective_food_volume_ratio( nuts ) == Approx( expect_ratio ).margin( 0.01f ) ); diff --git a/tests/iteminfo_test.cpp b/tests/iteminfo_test.cpp index 8a20ac71418bb..321abfa25db4a 100644 --- a/tests/iteminfo_test.cpp +++ b/tests/iteminfo_test.cpp @@ -1673,7 +1673,7 @@ TEST_CASE( "nutrients in food", "[iteminfo][food]" ) "--\n" "Nutrition will vary with chosen ingredients.\n" "Calories (kcal):" - " 127-469" + " 126-467" " Quench: 0\n" ); CHECK( item_info_str( ice_cream, { iteminfo_parts::FOOD_VITAMINS } ) == diff --git a/tests/submap_load_test.cpp b/tests/submap_load_test.cpp index 7ad3364e4a9b3..02857b49a08ca 100644 --- a/tests/submap_load_test.cpp +++ b/tests/submap_load_test.cpp @@ -16,7 +16,7 @@ static const point random_pt( 4, 7 ); static std::istringstream submap_empty_ss( "{\n" - " \"version\": 31,\n" + " \"version\": 32,\n" " \"coordinates\": [ 0, 0, 0 ],\n" " \"turn_last_touched\": 0,\n" " \"temperature\": 0,\n" @@ -35,7 +35,7 @@ static std::istringstream submap_empty_ss( ); static std::istringstream submap_terrain_rle_ss( "{\n" - " \"version\": 31,\n" + " \"version\": 32,\n" " \"coordinates\": [ 0, 0, 0 ],\n" " \"turn_last_touched\": 0,\n" " \"temperature\": 0,\n" @@ -63,7 +63,7 @@ static std::istringstream submap_terrain_rle_ss( ); static std::istringstream submap_furniture_ss( "{\n" - " \"version\": 31,\n" + " \"version\": 32,\n" " \"coordinates\": [ 0, 0, 0 ],\n" " \"turn_last_touched\": 0,\n" " \"temperature\": 0,\n" @@ -88,7 +88,7 @@ static std::istringstream submap_furniture_ss( ); static std::istringstream submap_trap_ss( "{\n" - " \"version\": 31,\n" + " \"version\": 32,\n" " \"coordinates\": [ 0, 0, 0 ],\n" " \"turn_last_touched\": 0,\n" " \"temperature\": 0,\n" @@ -113,7 +113,7 @@ static std::istringstream submap_trap_ss( ); static std::istringstream submap_rad_ss( "{\n" - " \"version\": 31,\n" + " \"version\": 32,\n" " \"coordinates\": [ 0, 0, 0 ],\n" " \"turn_last_touched\": 0,\n" " \"temperature\": 0,\n" @@ -142,7 +142,7 @@ static std::istringstream submap_rad_ss( ); static std::istringstream submap_item_ss( "{\n" - " \"version\": 31,\n" + " \"version\": 32,\n" " \"coordinates\": [ 0, 0, 0 ],\n" " \"turn_last_touched\": 0,\n" " \"temperature\": 0,\n" @@ -274,7 +274,7 @@ static std::istringstream submap_item_ss( ); static std::istringstream submap_field_ss( "{\n" - " \"version\": 31,\n" + " \"version\": 32,\n" " \"coordinates\": [ 0, 0, 0 ],\n" " \"turn_last_touched\": 0,\n" " \"temperature\": 0,\n" @@ -300,7 +300,7 @@ static std::istringstream submap_field_ss( ); static std::istringstream submap_graffiti_ss( "{\n" - " \"version\": 31,\n" + " \"version\": 32,\n" " \"coordinates\": [ 0, 0, 0 ],\n" " \"turn_last_touched\": 0,\n" " \"temperature\": 0,\n" @@ -326,7 +326,7 @@ static std::istringstream submap_graffiti_ss( ); static std::istringstream submap_spawns_ss( "{\n" - " \"version\": 31,\n" + " \"version\": 32,\n" " \"coordinates\": [ 0, 0, 0 ],\n" " \"turn_last_touched\": 0,\n" " \"temperature\": 0,\n" @@ -351,7 +351,7 @@ static std::istringstream submap_spawns_ss( ); static std::istringstream submap_vehicle_ss( "{\n" - " \"version\": 31,\n" + " \"version\": 32,\n" " \"coordinates\": [ 0, 0, 0 ],\n" " \"turn_last_touched\": 0,\n" " \"temperature\": 0,\n" @@ -554,7 +554,7 @@ static std::istringstream submap_vehicle_ss( ); static std::istringstream submap_construction_ss( "{\n" - " \"version\": 31,\n" + " \"version\": 32,\n" " \"coordinates\": [ 0, 0, 0 ],\n" " \"turn_last_touched\": 0,\n" " \"temperature\": 0,\n" @@ -650,7 +650,7 @@ static std::istringstream submap_construction_ss( ); static std::istringstream submap_computer_ss( "{\n" - " \"version\": 31,\n" + " \"version\": 32,\n" " \"coordinates\": [ 0, 0, 0 ],\n" " \"turn_last_touched\": 0,\n" " \"temperature\": 0,\n" @@ -698,7 +698,7 @@ static std::istringstream submap_computer_ss( ); static std::istringstream submap_cosmetic_ss( "{\n" - " \"version\": 31,\n" + " \"version\": 32,\n" " \"coordinates\": [ 0, 0, 0 ],\n" " \"turn_last_touched\": 0,\n" " \"temperature\": 0,\n" @@ -745,7 +745,7 @@ static JsonIn submap_cosmetic( submap_cosmetic_ss ); static void load_from_jsin( submap &sm, JsonIn &jsin ) { // Ensure that the JSON is up to date for our savegame version - REQUIRE( savegame_version == 31 ); + REQUIRE( savegame_version == 32 ); jsin.start_object(); int version = 0; while( !jsin.end_object() ) { From 085b4e376f38cd77d6b11ce5595d6d4530616068 Mon Sep 17 00:00:00 2001 From: anothersimulacrum Date: Thu, 21 Jan 2021 21:56:30 -0800 Subject: [PATCH 3/4] Convert character to use calories internally Having more precision enables removing rng from metabolism, without losing detail (the rng was introduced through roll_remainder). Calorie precision is only available privately - nothing outside of the metabolism needs that level of precision. --- src/character.cpp | 28 ++++++++++++++++++++-------- src/character.h | 9 +++++++++ src/savegame_json.cpp | 4 ++++ 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/src/character.cpp b/src/character.cpp index 3e00e0dc17958..e633474c8968f 100644 --- a/src/character.cpp +++ b/src/character.cpp @@ -454,7 +454,8 @@ Character::Character() : update_type_of_scent( true ); pkill = 0; // 45 days to starve to death - healthy_calories = 55000; + // 55 Mcal or 55k kcal + healthy_calories = 55'000'000; stored_calories = healthy_calories; initialize_stomach_contents(); @@ -5019,7 +5020,7 @@ void Character::mod_healthy_mod( int nhealthy_mod, int cap ) int Character::get_stored_kcal() const { - return stored_calories; + return stored_calories / 1000; } static std::string exert_lvl_to_str( float level ) @@ -5054,8 +5055,8 @@ std::string Character::debug_weary_info() const float bmi = get_bmi(); return string_format( "Weariness: %s Max Full Exert: %s Mult: %g\nBMR: %d Intake: %d Tracker: %d Thresh: %d At: %d\nCal: %d/%d Fatigue: %d Morale: %d Wgt: %d (BMI %.1f)", - amt, max_act, move_mult, bmr, intake, input, thresh, current, stored_calories, - healthy_calories, fatigue, morale, weight, bmi ); + amt, max_act, move_mult, bmr, intake, input, thresh, current, get_stored_kcal(), + get_healthy_kcal(), fatigue, morale, weight, bmi ); } void weariness_tracker::clear() @@ -5068,6 +5069,12 @@ void weariness_tracker::clear() void Character::mod_stored_kcal( int nkcal ) { + mod_stored_calories( nkcal * 1000 ); +} + +void Character::mod_stored_calories( int ncal ) +{ + int nkcal = ncal / 1000; if( nkcal > 0 ) { add_gained_calories( nkcal ); weary.intake += nkcal; @@ -5076,7 +5083,7 @@ void Character::mod_stored_kcal( int nkcal ) // nkcal is negative, we need positive weary.tracker -= nkcal; } - set_stored_kcal( stored_calories + nkcal ); + set_stored_calories( stored_calories + ncal ); } void Character::mod_stored_nutr( int nnutr ) @@ -5087,8 +5094,13 @@ void Character::mod_stored_nutr( int nnutr ) void Character::set_stored_kcal( int kcal ) { - if( stored_calories != kcal ) { - stored_calories = kcal; + set_stored_calories( kcal * 1000 ); +} + +void Character::set_stored_calories( int cal ) +{ + if( stored_calories != cal ) { + stored_calories = cal; //some mutant change their max_hp according to their bmi recalc_hp(); @@ -5097,7 +5109,7 @@ void Character::set_stored_kcal( int kcal ) int Character::get_healthy_kcal() const { - return healthy_calories; + return healthy_calories / 1000; } float Character::get_kcal_percent() const diff --git a/src/character.h b/src/character.h index 4c31fe7eae200..b1b1cb4a77eec 100644 --- a/src/character.h +++ b/src/character.h @@ -517,6 +517,14 @@ class Character : public Creature, public visitable virtual void set_fatigue( fatigue_levels nfatigue ); virtual void set_sleep_deprivation( int nsleep_deprivation ); + protected: + + // These accept values in calories, 1/1000s of kcals (or Calories) + virtual void mod_stored_calories( int ncal ); + virtual void set_stored_calories( int cal ); + + public: + void mod_stat( const std::string &stat, float modifier ) override; int get_standard_stamina_cost( item *thrown_item = nullptr ); @@ -2911,6 +2919,7 @@ class Character : public Creature, public visitable int old_weary_level = 0; /// @brief Needs (hunger, starvation, thirst, fatigue, etc.) + // Stored calories is a value in 'calories' - 1/1000s of kcals (or Calories) int stored_calories; int healthy_calories; diff --git a/src/savegame_json.cpp b/src/savegame_json.cpp index 9e72cfcb7a6a9..f42cf5a39b3a6 100644 --- a/src/savegame_json.cpp +++ b/src/savegame_json.cpp @@ -561,6 +561,10 @@ void Character::load( const JsonObject &data ) data.read( "weary", weary ); data.read( "sleep_deprivation", sleep_deprivation ); data.read( "stored_calories", stored_calories ); + // stored_calories was changed from being in kcal to being in just cal + if( savegame_loading_version <= 31 ) { + stored_calories *= 1000; + } data.read( "radiation", radiation ); data.read( "oxygen", oxygen ); data.read( "pkill", pkill ); From c0a7b9ee3e2fb23b4b3d354ae9f26249f2c50bfc Mon Sep 17 00:00:00 2001 From: anothersimulacrum Date: Thu, 21 Jan 2021 21:56:56 -0800 Subject: [PATCH 4/4] Remove randomness in metabolism With more precision now, we can drop rng based rounding! Adjust the two places where this occurs, then adjust all tests to fit. The weariness tests actually pass with margins of 0, but margin of 5 is more safe for extenuating circumstances (e.g. unordered tests). --- src/character.cpp | 3 ++- src/stomach.cpp | 3 ++- tests/char_biometrics_test.cpp | 7 +++--- tests/weary_test.cpp | 42 +++++++++++++++++----------------- 4 files changed, 28 insertions(+), 27 deletions(-) diff --git a/src/character.cpp b/src/character.cpp index e633474c8968f..18cabb780f7b0 100644 --- a/src/character.cpp +++ b/src/character.cpp @@ -5721,7 +5721,8 @@ void Character::update_stomach( const time_point &from, const time_point &to ) if( !foodless && rates.hunger > 0.0f ) { mod_hunger( roll_remainder( rates.hunger * five_mins ) ); // instead of hunger keeping track of how you're living, burn calories instead - mod_stored_kcal( -roll_remainder( five_mins * kcal_per_time ) ); + // Explicitly floor it here, the int cast will do so anyways + mod_stored_calories( -std::floor( five_mins * kcal_per_time * 1000 ) ); } } // if npc_no_food no need to calc hunger, and set hunger_effect diff --git a/src/stomach.cpp b/src/stomach.cpp index 85500a2699a39..72ed8a7fbed9c 100644 --- a/src/stomach.cpp +++ b/src/stomach.cpp @@ -244,7 +244,8 @@ stomach_digest_rates stomach_contents::get_digest_rates( const needs_rates &meta // 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_calories = roll_remainder( metabolic_rates.kcal / 24.0 * metabolic_rates.hunger ) * 1000; + // Explicitly floor it, because casting it to an int will do so anyways + rates.min_calories = std::floor( metabolic_rates.kcal / 24.0 * metabolic_rates.hunger * 1000 ); rates.percent_kcal = 0.05f * metabolic_rates.hunger; rates.min_vitamin = std::round( 100.0 / 24.0 * metabolic_rates.hunger ); rates.percent_vitamin = 0.05f * metabolic_rates.hunger; diff --git a/tests/char_biometrics_test.cpp b/tests/char_biometrics_test.cpp index ca40794915362..206109d9fa807 100644 --- a/tests/char_biometrics_test.cpp +++ b/tests/char_biometrics_test.cpp @@ -542,10 +542,9 @@ TEST_CASE( "activity levels and calories in daily diary", "[avatar][biometrics][ test_activity_duration( dummy, ACTIVE_EXERCISE, 15_minutes ); test_activity_duration( dummy, EXTRA_EXERCISE, 10_minutes ); - // Spent calories are randomized; get expected spent/net from expected gain - int expect_gained_kcal = 1286; // FIXME: Could this be random too? - int expect_net_kcal = dummy.get_stored_kcal() - 55000; - int expect_spent_kcal = expect_gained_kcal - expect_net_kcal; + int expect_gained_kcal = 1283; + int expect_net_kcal = 551; + int expect_spent_kcal = 732; CHECK( condensed_spaces( dummy.total_daily_calories_string() ) == string_format( " Minutes at each exercise level Calories per day\n" diff --git a/tests/weary_test.cpp b/tests/weary_test.cpp index 7545524f34c42..318e55a734d39 100644 --- a/tests/weary_test.cpp +++ b/tests/weary_test.cpp @@ -55,7 +55,7 @@ TEST_CASE( "weary_assorted_tasks", "[weary][activities]" ) INFO( info.summarize() ); INFO( guy.debug_weary_info() ); REQUIRE( !info.empty() ); - CHECK( info.transition_minutes( 0, 1, 365_minutes ) == Approx( 365 ).margin( 5 ) ); + CHECK( info.transition_minutes( 0, 1, 370_minutes ) == Approx( 370 ).margin( 5 ) ); CHECK( guy.weariness_level() == 1 ); } @@ -66,10 +66,10 @@ TEST_CASE( "weary_assorted_tasks", "[weary][activities]" ) INFO( info.summarize() ); INFO( guy.debug_weary_info() ); REQUIRE( !info.empty() ); - CHECK( info.transition_minutes( 0, 1, 120_minutes ) == Approx( 120 ).margin( 5 ) ); - CHECK( info.transition_minutes( 1, 2, 250_minutes ) == Approx( 250 ).margin( 5 ) ); + CHECK( info.transition_minutes( 0, 1, 115_minutes ) == Approx( 115 ).margin( 5 ) ); + CHECK( info.transition_minutes( 1, 2, 255_minutes ) == Approx( 255 ).margin( 5 ) ); CHECK( info.transition_minutes( 2, 3, 360_minutes ) == Approx( 360 ).margin( 5 ) ); - CHECK( info.transition_minutes( 3, 4, 465_minutes ) == Approx( 465 ).margin( 5 ) ); + CHECK( info.transition_minutes( 3, 4, 470_minutes ) == Approx( 470 ).margin( 5 ) ); CHECK( guy.weariness_level() == 4 ); INFO( "\nDigging Pits 12 hours:" ); @@ -77,12 +77,12 @@ TEST_CASE( "weary_assorted_tasks", "[weary][activities]" ) INFO( info.summarize() ); INFO( guy.debug_weary_info() ); REQUIRE( !info.empty() ); - CHECK( info.transition_minutes( 0, 1, 125_minutes ) == Approx( 125 ).margin( 5 ) ); - CHECK( info.transition_minutes( 1, 2, 250_minutes ) == Approx( 250 ).margin( 5 ) ); + CHECK( info.transition_minutes( 0, 1, 120_minutes ) == Approx( 120 ).margin( 5 ) ); + CHECK( info.transition_minutes( 1, 2, 245_minutes ) == Approx( 245 ).margin( 5 ) ); CHECK( info.transition_minutes( 2, 3, 355_minutes ) == Approx( 355 ).margin( 5 ) ); - CHECK( info.transition_minutes( 3, 4, 460_minutes ) == Approx( 460 ).margin( 5 ) ); - CHECK( info.transition_minutes( 4, 5, 590_minutes ) == Approx( 590 ).margin( 5 ) ); - CHECK( guy.weariness_level() == 6 ); + CHECK( info.transition_minutes( 3, 4, 465_minutes ) == Approx( 465 ).margin( 5 ) ); + CHECK( info.transition_minutes( 4, 5, 595_minutes ) == Approx( 595 ).margin( 5 ) ); + CHECK( guy.weariness_level() == 5 ); } } @@ -123,8 +123,8 @@ TEST_CASE( "weary_recovery", "[weary][activities]" ) INFO( info.summarize() ); INFO( guy.debug_weary_info() ); REQUIRE( !info.empty() ); - CHECK( info.transition_minutes( 4, 3, 550_minutes ) == Approx( 550 ).margin( 0 ) ); - CHECK( info.transition_minutes( 3, 2, 670_minutes ) == Approx( 670 ).margin( 0 ) ); + CHECK( info.transition_minutes( 4, 3, 520_minutes ) == Approx( 520 ).margin( 5 ) ); + CHECK( info.transition_minutes( 3, 2, 670_minutes ) == Approx( 670 ).margin( 5 ) ); CHECK( guy.weariness_level() == 1 ); } @@ -135,10 +135,10 @@ TEST_CASE( "weary_recovery", "[weary][activities]" ) INFO( info.summarize() ); INFO( guy.debug_weary_info() ); REQUIRE( !info.empty() ); - CHECK( info.transition_minutes( 0, 1, 325_minutes ) == Approx( 325 ).margin( 5 ) ); - CHECK( info.transition_minutes( 1, 2, 625_minutes ) == Approx( 625 ).margin( 5 ) ); + CHECK( info.transition_minutes( 0, 1, 335_minutes ) == Approx( 335 ).margin( 5 ) ); + CHECK( info.transition_minutes( 1, 2, 645_minutes ) == Approx( 645 ).margin( 5 ) ); CHECK( info.transition_minutes( 2, 1, 740_minutes ) == Approx( 740 ).margin( 5 ) ); - CHECK( info.transition_minutes( 1, 0, 990_minutes ) == Approx( 990 ).margin( 5 ) ); + CHECK( info.transition_minutes( 1, 0, 980_minutes ) == Approx( 980 ).margin( 5 ) ); } } @@ -167,14 +167,14 @@ TEST_CASE( "weary_24h_tasks", "[weary][activities]" ) INFO( info.summarize() ); INFO( guy.debug_weary_info() ); REQUIRE( !info.empty() ); - CHECK( info.transition_minutes( 0, 1, 130_minutes ) == Approx( 130 ).margin( 5 ) ); - CHECK( info.transition_minutes( 1, 2, 255_minutes ) == Approx( 255 ).margin( 5 ) ); + CHECK( info.transition_minutes( 0, 1, 125_minutes ) == Approx( 125 ).margin( 5 ) ); + CHECK( info.transition_minutes( 1, 2, 260_minutes ) == Approx( 260 ).margin( 5 ) ); CHECK( info.transition_minutes( 2, 3, 360_minutes ) == Approx( 360 ).margin( 5 ) ); - CHECK( info.transition_minutes( 3, 4, 465_minutes ) == Approx( 465 ).margin( 5 ) ); - CHECK( info.transition_minutes( 4, 5, 585_minutes ) == Approx( 590 ).margin( 5 ) ); - CHECK( info.transition_minutes( 5, 6, 725_minutes ) == Approx( 725 ).margin( 10 ) ); - CHECK( info.transition_minutes( 6, 7, 810_minutes ) == Approx( 810 ).margin( 10 ) ); - CHECK( info.transition_minutes( 7, 8, 890_minutes ) == Approx( 890 ).margin( 10 ) ); + CHECK( info.transition_minutes( 3, 4, 470_minutes ) == Approx( 470 ).margin( 5 ) ); + CHECK( info.transition_minutes( 4, 5, 600_minutes ) == Approx( 600 ).margin( 5 ) ); + CHECK( info.transition_minutes( 5, 6, 725_minutes ) == Approx( 725 ).margin( 5 ) ); + CHECK( info.transition_minutes( 6, 7, 835_minutes ) == Approx( 835 ).margin( 5 ) ); + CHECK( info.transition_minutes( 7, 8, 910_minutes ) == Approx( 910 ).margin( 5 ) ); // TODO: You should collapse from this - currently we // just get really high levels of weariness CHECK( guy.weariness_level() > 8 );