Skip to content

Commit

Permalink
PR #2: Replacing the CAL/kg row of the (E)at menu with a more meaning…
Browse files Browse the repository at this point in the history
…ful Satiety row. (CleverRaven#44490)
  • Loading branch information
gkarfakis19 authored Oct 2, 2020
1 parent 43287e6 commit ba75c03
Show file tree
Hide file tree
Showing 7 changed files with 192 additions and 20 deletions.
25 changes: 25 additions & 0 deletions data/mods/TEST_DATA/items.json
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,31 @@
"smoking_result": "dry_fruit",
"vitamins": [ [ "vitA", 2 ], [ "vitC", 14 ], [ "iron", 1 ] ]
},
{
"id": "test_egg",
"type": "COMESTIBLE",
"comestible_type": "FOOD",
"category": "food",
"name": { "str": "test egg" },
"description": "Test egg, very much like a bird egg, but a little too perfect. Does not need to be cooked.",
"volume": "50 ml",
"weight": "40 g",
"color": "brown",
"spoils_in": "7 days",
"symbol": "o",
"quench": 4,
"healthy": 1,
"calories": 80,
"price": 44,
"price_postapoc": 50,
"material": [ "egg" ],
"fun": -8,
"//": "Omitting the RAW flag so calories will be taken at face value.",
"flags": [ "FREEZERBURN" ],
"rot_spawn": "GROUP_EGG_BIRD_WILD",
"vitamins": [ [ "vitA", 9 ], [ "calcium", 3 ], [ "iron", 4 ], [ "vitB", 21 ] ],
"rot_spawn_chance": 50
},
{
"id": "test_jug_plastic",
"type": "GENERIC",
Expand Down
4 changes: 4 additions & 0 deletions src/character.h
Original file line number Diff line number Diff line change
Expand Up @@ -2255,6 +2255,10 @@ class Character : public Creature, public visitable<Character>
void modify_addiction( const islot_comestible &comest );
/** Used to apply health modifications from food and medication **/
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 to display how filling a food is. */
int compute_calories_per_effective_volume( const item &food ) const;
/** Handles the effects of consuming an item */
bool consume_effects( item &food );
/** Check character's capability of consumption overall */
Expand Down
45 changes: 37 additions & 8 deletions src/consumption.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1254,6 +1254,42 @@ void Character::modify_morale( item &food, const int nutr )
}
}


// Used when determining stomach fullness from eating.
double Character::compute_effective_food_volume_ratio( const item &food ) const
{
const nutrients food_nutrients = compute_effective_nutrients( food );
units::mass food_weight = ( food.weight() / food.count() );
double ratio = 1.0f;
if( units::to_gram( food_weight ) != 0 ) {
ratio = std::max( static_cast<double>( food_nutrients.kcal ) / units::to_gram( food_weight ), 1.0 );
if( ratio > 3.0f ) {
ratio = std::sqrt( 3 * ratio );
}
}
return ratio;
}

// Used when displaying effective food satiation values.
int Character::compute_calories_per_effective_volume( const item &food ) const
{
/* Understanding how Calories Per Effective Volume are calculated requires a dive into the
stomach fullness source code. Look at issue #44365*/
const nutrients nutr = compute_effective_nutrients( food );
const int kcalories = nutr.kcal;
units::volume water_vol = ( food.type->comestible->quench > 0 ) ? food.type->comestible->quench *
5_ml : 0_ml;
// Water volume is ignored.
units::volume food_vol = food.volume() - water_vol * food.count();
// Divide by 1000 to convert to L. Final quantity is essentially dimensionless, so unit of measurement does not matter.
const double converted_volume = round_up( ( static_cast<float>( food_vol.value() ) / food.count() )
* 0.001, 2 );
const double energy_density_ratio = compute_effective_food_volume_ratio( food );
const double effective_volume = converted_volume * energy_density_ratio;
return std::round( kcalories / effective_volume );
}


bool Character::consume_effects( item &food )
{
if( !food.is_comestible() ) {
Expand Down Expand Up @@ -1378,14 +1414,7 @@ bool Character::consume_effects( item &food )
5_ml : 0_ml;
units::volume food_vol = food.base_volume() - water_vol;
units::mass food_weight = ( food.weight() / food.count() );
double ratio = 1.0f;
if( units::to_gram( food_weight ) != 0 ) {
ratio = std::max( static_cast<double>( food_nutrients.kcal ) / units::to_gram( food_weight ), 1.0 );
if( ratio > 3.0f ) {
ratio = std::sqrt( 3 * ratio );
}
}

const double ratio = compute_effective_food_volume_ratio( food );
food_summary ingested{
water_vol,
food_vol * ratio,
Expand Down
35 changes: 23 additions & 12 deletions src/game_inventory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,7 @@ item_location game_menus::inv::disassemble( Character &p )
_( "You don't have any items you could disassemble." ) );
}


class comestible_inventory_preset : public inventory_selector_preset
{
public:
Expand Down Expand Up @@ -534,22 +535,32 @@ class comestible_inventory_preset : public inventory_selector_preset
return string_format( _( "%.2f%s" ), converted_volume, volume_units_abbr() );
}, _( "VOLUME" ) );

append_cell( [&p]( const item_location & loc ) {
// Title of this cell. Defined here in order to preserve proper padding and alignment of values in the lambda.
const std::string this_cell_title = _( "SATIETY" );
append_cell( [&p, this_cell_title]( const item_location & loc ) {
const item &it = *loc;
const int charges = std::max( it.charges, 1 );
const double converted_weight = convert_weight( it.weight() / charges );
if( converted_weight == 0 ) {
return std::string( "---" );
// Quit prematurely if the item is not food.
if( !it.type->comestible ) {
return std::string();
}
const nutrients nutr = p.compute_effective_nutrients( *loc );
const int kcalories = nutr.kcal;
// Experimental: if calories are 0 (medicine, batteries etc), don't display anything.
if( kcalories == 0 ) {
const int calories_per_effective_volume = p.compute_calories_per_effective_volume( it );
// Show empty cell instead of 0.
if( calories_per_effective_volume == 0 ) {
return std::string();
}
const int calpergr = int( std::round( kcalories / converted_weight ) );
return string_format( _( "%d" ), calpergr );
}, _( "CAL/kg" ) );
/* This is for screen readers. I will make a PR to discuss what these prerequisites could be -
bio_digestion, selfaware, high cooking skill etc*/
constexpr bool ARBITRARY_PREREQUISITES_TO_BE_DETERMINED_IN_THE_FUTURE = false;
if( ARBITRARY_PREREQUISITES_TO_BE_DETERMINED_IN_THE_FUTURE ) {
return string_format( "%d", calories_per_effective_volume );
}
std::string result = satiety_bar( calories_per_effective_volume );
// if this_cell_title is larger than 5 characters, pad to match its length, preserving alignment.
if( utf8_width( this_cell_title ) > 5 ) {
result += std::string( utf8_width( this_cell_title ) - 5, ' ' );
}
return result;
}, _( this_cell_title ) );

Character &player_character = get_player_character();
append_cell( [&player_character]( const item_location & loc ) {
Expand Down
17 changes: 17 additions & 0 deletions src/output.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1965,6 +1965,23 @@ void insert_table( const catacurses::window &w, int pad, int line, int columns,
wattroff( w, FG );
}


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 int max_cal_per_effective_vol = 1500;
//Scaling the values.
const int scaled_max = std::sqrt( max_cal_per_effective_vol ) / 4;
const int scaled_cal = std::sqrt( calpereffv ) / 4;
const std::pair<std::string, nc_color> nourishment_bar = get_bar(
scaled_cal, scaled_max, 5, true );
// Colorize the bar.
std::string result = colorize( nourishment_bar.first, nourishment_bar.second );
// Pad to 5 characters with dots.
result += std::string( 5 - nourishment_bar.first.length(), '.' );
return result;
}

scrollingcombattext::cSCT::cSCT( const point &p_pos, const direction p_oDir,
const std::string &p_sText, const game_message_type p_gmt,
const std::string &p_sText2, const game_message_type p_gmt2,
Expand Down
1 change: 1 addition & 0 deletions src/output.h
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@ int right_print( const catacurses::window &w, int line, int right_indent,
void insert_table( const catacurses::window &w, int pad, int line, int columns,
const nc_color &FG, const std::string &divider, bool r_align,
const std::vector<std::string> &data );
std::string satiety_bar( int calpereffv );
void scrollable_text( const std::function<catacurses::window()> &init_window,
const std::string &title, const std::string &text );
std::string name_and_value( const std::string &name, int value, int field_width );
Expand Down
85 changes: 85 additions & 0 deletions tests/comestible_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include "item.h"
#include "item_contents.h"
#include "itype.h"
#include "output.h"
#include "recipe.h"
#include "recipe_dictionary.h"
#include "requirements.h"
Expand Down Expand Up @@ -178,3 +179,87 @@ TEST_CASE( "cooked_veggies_get_correct_calorie_prediction", "[recipe]" )
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
// used as a multiplier for the food volume when it is eaten, based on the energy density
// (kcal/gram) of the food, as follows:
//
// - low-energy food (0.0 < kcal/gram < 1.0) returns 1.0
// - medium-energy food (1.0 < kcal/gram < 3.0) returns (kcal/gram)
// - high-energy food (3.0 < kcal/gram) returns sqrt( 3 * kcal/gram )
//
// 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.
//
TEST_CASE( "effective food volume and satiety", "[character][food][satiety]" )
{
const Character &u = get_player_character();
double expect_ratio;

// Apple: 95 kcal / 200 g (1 serving)
const item apple( "test_apple" );
const nutrients apple_nutr = u.compute_effective_nutrients( apple );
REQUIRE( apple.count() == 1 );
REQUIRE( apple.weight() == 200_gram );
REQUIRE( apple.volume() == 250_ml );
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 ) == 396 );
// NOLINTNEXTLINE(cata-text-style): verbatim ellipses necessary for validation
CHECK( satiety_bar( 396 ) == "<color_c_yellow>||</color>..." );

// Egg: 80 kcal / 40 g (1 serving)
const item egg( "test_egg" );
const nutrients egg_nutr = u.compute_effective_nutrients( egg );
REQUIRE( egg.count() == 1 );
REQUIRE( egg.weight() == 40_gram );
REQUIRE( egg.volume() == 50_ml );
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 ) == 1333 );
CHECK( satiety_bar( 1333 ) == "<color_c_green>|||||</color>" );

// Pine nuts: 202 kcal / 30 g (4 servings)
const item nuts( "test_pine_nuts" );
const nutrients nuts_nutr = u.compute_effective_nutrients( nuts );
// If food count > 1, total weight is divided by count before computing kcal/gram
REQUIRE( nuts.count() == 4 );
REQUIRE( nuts.weight() == 120_gram );
REQUIRE( nuts.volume() == 250_ml );
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 ) );
CHECK( u.compute_calories_per_effective_volume( nuts ) == 642 );
CHECK( satiety_bar( 642 ) == "<color_c_light_green>|||</color>.." );
}

// satiety_bar returns a colorized string indicating a satiety level, similar to hit point bars
// where "....." is minimum (~ 0) and "|||||" is maximum (~ 1500)
//
TEST_CASE( "food satiety bar", "[character][food][satiety]" )
{
// NOLINTNEXTLINE(cata-text-style): verbatim ellipses necessary for validation
CHECK( satiety_bar( 0 ) == "<color_c_red></color>....." );
// NOLINTNEXTLINE(cata-text-style): verbatim ellipses necessary for validation
CHECK( satiety_bar( 100 ) == "<color_c_light_red>|</color>...." );
// 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( 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_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>" );
}

0 comments on commit ba75c03

Please sign in to comment.