Skip to content

Commit

Permalink
Merge pull request #78206 from osuphobia/Modify-compute_calories_per_…
Browse files Browse the repository at this point in the history
…effective_volume

Audit `satiety`.
  • Loading branch information
Anton Burmistrov authored Dec 2, 2024
2 parents b6e57f5 + fad5e6a commit 6bcd824
Show file tree
Hide file tree
Showing 4 changed files with 71 additions and 36 deletions.
4 changes: 2 additions & 2 deletions src/character.h
Original file line number Diff line number Diff line change
Expand Up @@ -3401,8 +3401,8 @@ class Character : public Creature, public visitable
void modify_health( const islot_comestible &comest );
/** Used to compute how filling a food is.*/
double compute_effective_food_volume_ratio( const item &food ) const;
/** Used to calculate dry volume of a chewed food **/
units::volume masticated_volume( const item &food ) const;
/** Used to calculate water and dry volume of a chewed food **/
std::pair<units::volume, units::volume> masticated_volume( const item &food ) const;
/** Used to to display how filling a food is. */
int compute_calories_per_effective_volume( const item &food,
const nutrients *nutrient = nullptr ) const;
Expand Down
39 changes: 26 additions & 13 deletions src/consumption.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1519,10 +1519,10 @@ double Character::compute_effective_food_volume_ratio( const item &food ) const
return ratio;
}

// Remove the water volume from the food, as that gets absorbed and used as water.
// Separate the water volume from the food, as that gets absorbed and used as water.
// If the remaining dry volume of the food is less dense than water, crunch it down to a density equal to water.
// These maths are made easier by the fact that 1 g = 1 mL. Thanks, metric system.
units::volume Character::masticated_volume( const item &food ) const
std::pair<units::volume, units::volume> Character::masticated_volume( const item &food ) const
{
units::volume water_vol = ( food.get_comestible()->quench > 0 ) ? food.get_comestible()->quench *
5_ml : 0_ml;
Expand All @@ -1531,12 +1531,17 @@ units::volume Character::masticated_volume( const item &food ) const
units::mass food_dry_weight = food.weight() / std::max( 1, food.count() ) - water_weight;
units::volume food_dry_volume = food.volume() / std::max( 1, food.count() ) - water_vol;

if( units::to_milliliter( food_dry_volume ) != 0 &&
// Should not return minus volume.
if( units::to_milliliter( food_dry_volume ) <= 0 ) {
return { water_vol, 0_ml };
}

if( units::to_gram( food_dry_weight ) > 0 &&
units::to_gram( food_dry_weight ) < units::to_milliliter( food_dry_volume ) ) {
food_dry_volume = units::from_milliliter( units::to_gram( food_dry_weight ) );
}

return food_dry_volume;
return { water_vol, food_dry_volume };
}

// Used when displaying effective food satiation values.
Expand All @@ -1552,13 +1557,23 @@ int Character::compute_calories_per_effective_volume( const item &food,
} else {
kcalories = compute_effective_nutrients( food ).kcal();
}
double food_vol = round_up( units::to_liter( masticated_volume( food ) ), 2 );
const double energy_density_ratio = compute_effective_food_volume_ratio( food );
const double effective_volume = food_vol * energy_density_ratio;
if( kcalories == 0 && effective_volume == 0.0 ) {
if( kcalories == 0 ) {
// Quick bail out if it does not cotain any energy.
return 0;
}
return std::round( kcalories / effective_volume );
units::volume water_volume = masticated_volume( food ).first;
units::volume dry_volume = masticated_volume( food ).second;
// Water is digested more quickly than solid mass, see get_digest_rates().
// stomach_ratio is 0.278 for a default character, will be higher for mutants.
const double stomach_ratio = stomach.capacity( *this ) / ( 36.000 * 250_ml );
water_volume *= stomach_ratio;
const double energy_density_ratio = compute_effective_food_volume_ratio( food );
dry_volume *= energy_density_ratio;
const int effective_volume = dry_volume.value() + water_volume.value();
if( effective_volume == 0 ) {
return 2000;
}
return std::round( kcalories * 1000 / effective_volume );
}

static void activate_consume_eocs( Character &you, item &target )
Expand Down Expand Up @@ -1674,10 +1689,8 @@ bool Character::consume_effects( item &food )
}

nutrients food_nutrients = compute_effective_nutrients( food );
const units::volume water_vol = ( food.get_comestible()->quench > 0 ) ?
food.get_comestible()->quench *
5_ml : 0_ml;
units::volume food_vol = masticated_volume( food );
const units::volume water_vol = masticated_volume( food ).first;
units::volume food_vol = masticated_volume( food ).second;
if( food.count() == 0 ) {
debugmsg( "Tried to eat food with count of zero." );
return false;
Expand Down
2 changes: 1 addition & 1 deletion src/output.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2979,7 +2979,7 @@ void insert_table( const catacurses::window &w, int pad, int line, int columns,
std::string satiety_bar( const int calpereffv )
{
// Arbitrary max value we will cap our vague display to. Will be lower than the actual max value, but scaling fixes that.
constexpr float max_cal_per_effective_vol = 1500.0f;
constexpr float max_cal_per_effective_vol = 2000.0f;
// Scaling the values.
const float scaled_max = std::sqrt( max_cal_per_effective_vol );
const float scaled_cal = std::sqrt( calpereffv );
Expand Down
62 changes: 42 additions & 20 deletions tests/comestible_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ static const itype_id itype_marloss_seed( "marloss_seed" );

static const recipe_id recipe_veggy_wild_cooked( "veggy_wild_cooked" );

static const trait_id trait_GOURMAND( "GOURMAND" );

static const vitamin_id vitamin_mutagen( "mutagen" );
static const vitamin_id vitamin_mutagen_alpha( "mutagen_alpha" );
static const vitamin_id vitamin_mutagen_batrachian( "mutagen_batrachian" );
Expand Down Expand Up @@ -269,11 +271,13 @@ TEST_CASE( "cooked_veggies_get_correct_calorie_prediction", "[recipe]" )
//
// The Character::compute_calories_per_effective_volume function returns a dimensionless integer
// representing the "satiety" of the food, with higher numbers being more calorie-dense, and lower
// numbers being less so.
// numbers being less so. Water in food can be digested more quickly, but for a character with a
// bigger stomach, the difference will be smaller.
//
TEST_CASE( "effective_food_volume_and_satiety", "[character][food][satiety]" )
{
const Character &u = get_player_character();
Character &u = get_player_character();
u.unset_all_mutations();
double expect_ratio;

// Apple: 95 kcal / 200 g (1 serving)
Expand All @@ -283,10 +287,11 @@ TEST_CASE( "effective_food_volume_and_satiety", "[character][food][satiety]" )
REQUIRE( apple.weight() == 200_gram );
REQUIRE( apple.volume() == 250_ml );
REQUIRE( apple_nutr.kcal() == 95 );
REQUIRE( apple.get_comestible()->quench == 3 );
// 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 );
CHECK( satiety_bar( 500 ) == "<color_c_yellow>||\\</color>.." );
CHECK( u.compute_calories_per_effective_volume( apple ) == 502 );
CHECK( satiety_bar( 502 ) == "<color_c_yellow>||\\</color>.." );

// Egg: 80 kcal / 40 g (1 serving)
const item egg( "test_egg" );
Expand All @@ -295,10 +300,11 @@ TEST_CASE( "effective_food_volume_and_satiety", "[character][food][satiety]" )
REQUIRE( egg.weight() == 40_gram );
REQUIRE( egg.volume() == 50_ml );
REQUIRE( egg_nutr.kcal() == 80 );
REQUIRE( egg.get_comestible()->quench == 4 );
// 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 );
CHECK( satiety_bar( 2000 ) == "<color_c_green>|||||</color>" );
CHECK( u.compute_calories_per_effective_volume( egg ) == 1777 );
CHECK( satiety_bar( 1777 ) == "<color_c_green>||||\\</color>" );

// Pine nuts: 202 kcal / 30 g (4 servings)
const item nuts( "test_pine_nuts" );
Expand All @@ -308,11 +314,21 @@ TEST_CASE( "effective_food_volume_and_satiety", "[character][food][satiety]" )
REQUIRE( nuts.weight() == 120_gram );
REQUIRE( nuts.volume() == 250_ml );
REQUIRE( nuts_nutr.kcal() == 202 );
REQUIRE( nuts.get_comestible()->quench == -2 );
// 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 ) );
CHECK( u.compute_calories_per_effective_volume( nuts ) == 1498 );
CHECK( satiety_bar( 1498 ) == "<color_c_green>||||\\</color>" );
CHECK( u.compute_calories_per_effective_volume( nuts ) == 1507 );
CHECK( satiety_bar( 1507 ) == "<color_c_light_green>||||</color>." );
REQUIRE( u.mutate_towards( trait_GOURMAND ) );
// stomach size affects food with water...
CHECK( u.compute_effective_food_volume_ratio( egg ) == Approx( 2.0f ).margin( 0.01f ) );
CHECK( u.compute_calories_per_effective_volume( egg ) == 1568 );
CHECK( satiety_bar( 1568 ) == "<color_c_green>||||</color>." );
// but not food without water.
CHECK( u.compute_effective_food_volume_ratio( nuts ) == Approx( expect_ratio ).margin( 0.01f ) );
CHECK( u.compute_calories_per_effective_volume( nuts ) == 1507 );
CHECK( satiety_bar( 1507 ) == "<color_c_light_green>||||</color>." );
}

// satiety_bar returns a colorized string indicating a satiety level, similar to hit point bars
Expand All @@ -331,17 +347,23 @@ TEST_CASE( "food_satiety_bar", "[character][food][satiety]" )
// NOLINTNEXTLINE(cata-text-style): verbatim ellipses necessary for validation
CHECK( satiety_bar( 200 ) == "<color_c_light_red>|\\</color>..." );
// NOLINTNEXTLINE(cata-text-style): verbatim ellipses necessary for validation
CHECK( satiety_bar( 300 ) == "<color_c_yellow>||</color>..." );
CHECK( satiety_bar( 400 ) == "<color_c_yellow>||\\</color>.." );
CHECK( satiety_bar( 300 ) == "<color_c_yellow>|\\</color>..." );
// NOLINTNEXTLINE(cata-text-style): verbatim ellipses necessary for validation
CHECK( satiety_bar( 400 ) == "<color_c_yellow>||</color>..." );
CHECK( satiety_bar( 500 ) == "<color_c_yellow>||\\</color>.." );
CHECK( satiety_bar( 600 ) == "<color_c_light_green>|||</color>.." );
CHECK( satiety_bar( 700 ) == "<color_c_light_green>|||</color>.." );
CHECK( satiety_bar( 800 ) == "<color_c_light_green>|||\\</color>." );
CHECK( satiety_bar( 900 ) == "<color_c_light_green>|||\\</color>." );
CHECK( satiety_bar( 1000 ) == "<color_c_light_green>||||</color>." );
CHECK( satiety_bar( 1100 ) == "<color_c_light_green>||||</color>." );
CHECK( satiety_bar( 1200 ) == "<color_c_green>||||</color>." );
CHECK( satiety_bar( 1300 ) == "<color_c_green>||||\\</color>" );
CHECK( satiety_bar( 1400 ) == "<color_c_green>||||\\</color>" );
CHECK( satiety_bar( 1500 ) == "<color_c_green>|||||</color>" );
CHECK( satiety_bar( 600 ) == "<color_c_yellow>||\\</color>.." );
CHECK( satiety_bar( 700 ) == "<color_c_yellow>||\\</color>.." );
CHECK( satiety_bar( 800 ) == "<color_c_light_green>|||</color>.." );
CHECK( satiety_bar( 900 ) == "<color_c_light_green>|||</color>.." );
CHECK( satiety_bar( 1000 ) == "<color_c_light_green>|||\\</color>." );
CHECK( satiety_bar( 1100 ) == "<color_c_light_green>|||\\</color>." );
CHECK( satiety_bar( 1200 ) == "<color_c_light_green>|||\\</color>." );
CHECK( satiety_bar( 1300 ) == "<color_c_light_green>||||</color>." );
CHECK( satiety_bar( 1400 ) == "<color_c_light_green>||||</color>." );
CHECK( satiety_bar( 1500 ) == "<color_c_light_green>||||</color>." );
CHECK( satiety_bar( 1600 ) == "<color_c_green>||||</color>." );
CHECK( satiety_bar( 1700 ) == "<color_c_green>||||\\</color>" );
CHECK( satiety_bar( 1800 ) == "<color_c_green>||||\\</color>" );
CHECK( satiety_bar( 1900 ) == "<color_c_green>||||\\</color>" );
CHECK( satiety_bar( 2000 ) == "<color_c_green>|||||</color>" );
}

0 comments on commit 6bcd824

Please sign in to comment.