Skip to content

Commit

Permalink
Camp workers actually eat the food spent on them working (#71670)
Browse files Browse the repository at this point in the history
* Workers can eat

* The player can eat from the larder, too.

* Do not stuff NPCs like turkeys if they can't eat

* Workers are now fed

* Use a character vector for universal compatibility between player/NPCs

* IWYU

* Player can eat even when NPCs can't

* Sanity checking for player meal

* Handle 'deficit spending'

* Fix a little docs oopsie

* Clean up function comments
(Move back accidentally displaced comment for distribute_food)

* Faction screen shows you vitamins of concern

* Various clang fixes

* Fix display math

* More incantations to appease clang
  • Loading branch information
RenechCDDA authored Feb 20, 2024
1 parent 9d41514 commit 98de57e
Show file tree
Hide file tree
Showing 9 changed files with 178 additions and 26 deletions.
2 changes: 1 addition & 1 deletion doc/FACTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ Field | Meaning
`"known_by_u"` | boolean, whether the player has met members of the faction. Can be changed in play. Unknown factions will not be displayed in the faction menu.
`"size"` | integer, an approximate count of the members of the faction. Has no effect in play currently.
`"power"` | integer, an approximation of the faction's power. Has no effect in play currently.
`"food_supply"` | integer, the number of calories (not kilocalories!) available to the faction. Has no effect in play currently.
`"fac_food_supply"` | integer, the number of calories (not kilocalories!) available to the faction. Has no effect in play currently.
`"vitamins"` | array, *units* of vitamins available to this faction. This is not the same as RDA, see [the vitamins doc](VITAMIN.md) for more details. Has no effect in play currently.
`"wealth"` | integer, number of post-apocalyptic currency in cents that that faction has to purchase stuff. Serves as an upper limit on the amount of items restocked by a NPC of this faction with a defined shopkeeper_item_group (see NPCs.md)
`"currency"` | string, the item `"id"` of the faction's preferred currency. Faction shopkeeps will trade faction current at 100% value, for both selling and buying.
Expand Down
16 changes: 11 additions & 5 deletions src/basecamp.h
Original file line number Diff line number Diff line change
Expand Up @@ -226,16 +226,22 @@ class basecamp
bool set_sort_points();

// food utility
/// Takes all the food from the camp_food zone and increases the faction
/// food_supply
/// Changes the faction food supply by @ref change, returns the amount of kcal+vitamins consumed, a negative
/// total food supply hurts morale
/// Handles vitamin consumption when only a kcal value is supplied
nutrients camp_food_supply( nutrients &change );
/// LEGACY FUNCTION. Constructs a new nutrients struct in place and forwards it
nutrients camp_food_supply( int change = 0 );
/// LEGACY FUNCTION. Calculates raw kcal cost from duration of work and exercise, then forwards it to above
/// Constructs a new nutrients struct in place and forwards it. Passed argument should be in kilocalories.
nutrients camp_food_supply( int change );
/// Calculates raw kcal cost from duration of work and exercise, then forwards it to above
nutrients camp_food_supply( time_duration work, float exertion_level = NO_EXERCISE );
/// Evenly distributes the actual consumed food from a work project to the workers assigned to it
void feed_workers( const std::vector<std::reference_wrapper <Character>> &workers, nutrients food,
bool is_player_meal = false );
/// Helper, forwards to above
void feed_workers( Character &worker, nutrients food, bool is_player_meal = false );
void player_eats_meal();
/// Takes all the food from the camp_food zone and increases the faction
/// food_supply
bool distribute_food();
std::string name_display_of( const mission_id &miss_id );
void handle_hide_mission( const point &dir );
Expand Down
53 changes: 53 additions & 0 deletions src/faction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,55 @@ nc_color faction::food_supply_color()
}
}

std::pair<nc_color, std::string> faction::vitamin_stores( vitamin_type vit_type )
{
bool is_toxin = vit_type == vitamin_type::TOXIN;
const double days_of_food = food_supply.kcal() / 3000.0;
std::map<vitamin_id, int> stored_vits = food_supply.vitamins();
// First, pare down our search to only the relevant type
for( auto it = stored_vits.cbegin(); it != stored_vits.cend(); ) {
if( it->first->type() != vit_type ) {
it = stored_vits.erase( it );
} else {
++it;
}
}
if( stored_vits.empty() ) {
return std::pair<nc_color, std::string>( !is_toxin ? c_red : c_green, _( "None present (NONE)" ) );
}
std::vector<std::pair<vitamin_id, double>> vitamins;
// Iterate the map's content into a sortable container...
for( auto &vit : stored_vits ) {
int units_per_day = vit.first.obj().units_absorption_per_day();
double relative_intake = static_cast<double>( vit.second ) / static_cast<double>
( units_per_day ) / days_of_food;
// We use the inverse value for toxins, since they are bad.
if( is_toxin ) {
relative_intake = 1 / relative_intake;
}
vitamins.emplace_back( vit.first, relative_intake );
}
// Sort to find the worst-case scenario, lowest relative_intake is first
std::sort( vitamins.begin(), vitamins.end(), []( const auto & x, const auto & y ) {
return x.second > y.second;
} );
const double worst_intake = vitamins.at( 0 ).second;
std::string vit_name = vitamins.at( 0 ).first.obj().name();
std::string msg = is_toxin ? _( "(TRACE)" ) : _( "(PLENTY)" );
if( worst_intake <= 0.3 ) {
msg = is_toxin ? _( "(POISON)" ) : _( "(LACK)" );
return std::pair<nc_color, std::string>( c_red, string_format( _( "%1$s %2$s" ), vit_name,
msg ) );
}
if( worst_intake <= 1.0 ) {
msg = is_toxin ? _( "(DANGER)" ) : _( "(MEAGER)" );
return std::pair<nc_color, std::string>( c_yellow, string_format( _( "%1$s %2$s" ), vit_name,
msg ) );
}
return std::pair<nc_color, std::string>( c_green, string_format( _( "%1$s %2$s" ), vit_name,
msg ) );
}

faction_price_rule const *faction::get_price_rules( item const &it, npc const &guy ) const
{
auto const el = std::find_if(
Expand Down Expand Up @@ -519,6 +568,10 @@ void basecamp::faction_display( const catacurses::window &fac_w, const int width
yours->food_supply_text(), yours->food_supply.kcal() );
nc_color food_col = yours->food_supply_color();
mvwprintz( fac_w, point( width, ++y ), food_col, food_text );
std::pair<nc_color, std::string> vitamins = yours->vitamin_stores( vitamin_type::VITAMIN );
mvwprintz( fac_w, point( width, ++y ), vitamins.first, _( "Worst vitamin:" ) + vitamins.second );
std::pair<nc_color, std::string> toxins = yours->vitamin_stores( vitamin_type::TOXIN );
mvwprintz( fac_w, point( width, ++y ), toxins.first, _( "Worst toxin:" ) + toxins.second );
std::string bldg = next_upgrade( base_camps::base_dir, 1 );
std::string bldg_full = _( "Next Upgrade: " ) + bldg;
mvwprintz( fac_w, point( width, ++y ), col, bldg_full );
Expand Down
3 changes: 3 additions & 0 deletions src/faction.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include "stomach.h"
#include "translations.h"
#include "type_id.h"
#include "vitamin.h"

namespace catacurses
{
Expand Down Expand Up @@ -139,6 +140,8 @@ class faction : public faction_template
std::string food_supply_text();
nc_color food_supply_color();

std::pair<nc_color, std::string> vitamin_stores( vitamin_type vit );

faction_price_rule const *get_price_rules( item const &it, npc const &guy ) const;

bool has_relationship( const faction_id &guy_id, npc_factions::relationship flag ) const;
Expand Down
107 changes: 91 additions & 16 deletions src/faction_camp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
#include "npc.h"
#include "npctalk.h"
#include "omdata.h"
#include "options.h"
#include "output.h"
#include "overmap.h"
#include "overmap_ui.h"
Expand Down Expand Up @@ -351,6 +352,9 @@ static std::string mission_ui_activity_of( const mission_id &miss_id )
case Camp_Determine_Leadership:
return _( "Choose New Leader" );

case Camp_Have_Meal:
return _( "Have A Meal" );

case Camp_Hide_Mission:
return _( "Hide Mission(s)" );

Expand Down Expand Up @@ -1495,6 +1499,16 @@ void basecamp::get_available_missions( mission_data &mission_key, map &here )
mission_key.add( { miss_id, false }, name_display_of( miss_id ),
entry );
}
{
const mission_id miss_id = { Camp_Have_Meal, "", {}, base_dir };
entry = string_format( _( "Notes:\n"
"Eat some food from the larder.\n"
"Nutritional value depends on food stored in the larder.\n"
"Difficulty: N/A\n"
"Risk: None\n" ) );
mission_key.add( { miss_id, false }, name_display_of( miss_id ),
entry );
}
{
validate_assignees();
const mission_id miss_id = { Camp_Assign_Jobs, "", {}, base_dir };
Expand Down Expand Up @@ -1658,6 +1672,26 @@ void basecamp::choose_new_leader()
}
}

void basecamp::player_eats_meal()
{
int kcal_to_eat = 3000;
Character &you = get_player_character();
const int &food_available = you.get_faction()->food_supply.kcal();
if( you.stomach.contains() >= ( you.stomach.capacity( you ) / 2 ) ) {
popup( _( "You're way too full to eat a full meal right now." ) );
return;
}
if( food_available <= 0 ) {
popup( _( "You check storage for some food, but there is nothing but dust and cobwebs…" ) );
return;
} else if( food_available <= kcal_to_eat ) {
add_msg( _( "There's only one meal left. Guess that's dinner!" ) );
kcal_to_eat = food_available;
}
nutrients dinner = camp_food_supply( -kcal_to_eat );
feed_workers( you, dinner, true );
}

bool basecamp::handle_mission( const ui_mission_id &miss_id )
{
if( miss_id.id.id == No_Mission ) {
Expand All @@ -1678,6 +1712,10 @@ bool basecamp::handle_mission( const ui_mission_id &miss_id )
choose_new_leader();
break;

case Camp_Have_Meal:
player_eats_meal();
break;

case Camp_Hide_Mission:
handle_hide_mission( miss_id.id.dir.value() );
break;
Expand Down Expand Up @@ -1928,7 +1966,7 @@ npc_ptr basecamp::start_mission( const mission_id &miss_id, time_duration durati
if( comp != nullptr ) {
comp->companion_mission_time_ret = calendar::turn + duration;
if( must_feed ) {
camp_food_supply( duration, exertion_level );
feed_workers( *comp.get()->as_character(), camp_food_supply( duration, exertion_level ) );
}
if( !equipment.empty() ) {
map &target_map = get_camp_map();
Expand Down Expand Up @@ -2014,7 +2052,11 @@ comp_list basecamp::start_multi_mission( const mission_id &miss_id,
comp->companion_mission_time_ret = calendar::turn + work_days;
}
if( must_feed ) {
camp_food_supply( work_days * result.size(), making.exertion_level() );
std::vector<std::reference_wrapper <Character>> work_party;
for( npc_ptr &comp : result ) {
work_party.emplace_back( *comp.get()->as_character() );
}
feed_workers( work_party, camp_food_supply( work_days * result.size(), making.exertion_level() ) );
}
return result;
}
Expand Down Expand Up @@ -3722,14 +3764,12 @@ void basecamp::finish_return( npc &comp, const bool fixed_time, const std::strin
talk_function::companion_skill_trainer( comp, skill, mission_time, difficulty );
}

// companions subtracted food when they started the mission, but didn't mod their hunger for
// that food. so add it back in.
// Missions that are not fixed_time pay their food costs at the end, instead of up-front.
int need_food = time_to_food( mission_time - reserve_time );
faction *yours = get_player_character().get_faction();
if( yours->food_supply.kcal() < need_food ) {
popup( _( "Your companion seems disappointed that your pantry is empty…" ) );
}
int avail_food = std::min( need_food, yours->food_supply.kcal() ) + time_to_food( reserve_time );
// movng all the logic from talk_function::companion return here instead of polluting
// mission_companion
comp.reset_companion_mission();
Expand All @@ -3750,9 +3790,8 @@ void basecamp::finish_return( npc &comp, const bool fixed_time, const std::strin
g->reload_npcs();
validate_assignees();

camp_food_supply( -need_food );
comp.mod_hunger( -avail_food );
comp.mod_stored_kcal( avail_food );
// Missions that are not fixed_time can try to draw more food than is in the food supply
feed_workers( comp, camp_food_supply( -need_food ) );
if( has_water() ) {
comp.set_thirst( 0 );
}
Expand Down Expand Up @@ -5458,21 +5497,22 @@ nutrients basecamp::camp_food_supply( nutrients &change )
double percent_consumed = std::abs( static_cast<double>( change.calories ) ) /
yours->food_supply.calories;
consumed = yours->food_supply;
if( std::abs( change.calories ) > yours->food_supply.calories ) {
//Whoops, we don't have enough food. Empty the larder! No crumb shall go un-eaten!
yours->food_supply.calories -= change.calories;
yours->likes_u += yours->food_supply.kcal() / 1250;
yours->respects_u += yours->food_supply.kcal() / 625;
yours->trusts_u += yours->food_supply.kcal() / 625;
yours->food_supply *= 0;
return consumed;
}
consumed *= percent_consumed;
// Subtraction since we use the absolute value of change's calories to get the percent
yours->food_supply -= consumed;
return consumed;
}
yours->food_supply += change;
if( yours->food_supply.kcal() < 0 ) {
yours->likes_u += yours->food_supply.kcal() / 1250;
yours->respects_u += yours->food_supply.kcal() / 625;
yours->trusts_u += yours->food_supply.kcal() / 625;
yours->food_supply.calories = 0;
}

consumed = change;
//TODO: This return value is so that nutrients can actually be consumed by the workers instead of vanishing.
return consumed;
}

Expand All @@ -5489,6 +5529,40 @@ nutrients basecamp::camp_food_supply( time_duration work, float exertion_level )
return camp_food_supply( -time_to_food( work, exertion_level ) );
}

void basecamp::feed_workers( const std::vector<std::reference_wrapper <Character>> &workers,
nutrients food, bool is_player_meal )
{
const int num_workers = workers.size();
if( num_workers == 0 ) {
debugmsg( "feed_workers called without any workers to feed!" );
return;
}
if( !is_player_meal && get_option<bool>( "NO_NPC_FOOD" ) ) {
return;
}

// Split the food into equal sized portions.
food /= num_workers;
for( const auto &worker_reference : workers ) {
Character &worker = worker_reference.get();
worker.add_msg_if_player( _( "You grab a prepared meal from storage and chow down." ) );
units::volume filling_vol = std::max( 0_ml,
worker.stomach.capacity( worker ) / 2 - worker.stomach.contains() );
worker.stomach.ingest( food_summary{
0_ml,
filling_vol,
food
} );
}
}

void basecamp::feed_workers( Character &worker, nutrients food, bool is_player_meal )
{
std::vector<std::reference_wrapper <Character>> work_party;
work_party.emplace_back( worker );
feed_workers( work_party, std::move( food ), is_player_meal );
}

int time_to_food( time_duration work, float exertion_level )
{
const int days = to_hours<int>( work ) / 24;
Expand Down Expand Up @@ -5642,6 +5716,7 @@ std::string basecamp::name_display_of( const mission_id &miss_id )
// Faction camp tasks
case Camp_Distribute_Food:
case Camp_Determine_Leadership:
case Camp_Have_Meal:
case Camp_Hide_Mission:
case Camp_Reveal_Mission:
case Camp_Assign_Jobs:
Expand Down
6 changes: 6 additions & 0 deletions src/mission_companion.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ std::string enum_to_string<mission_kind>( mission_kind data )
case mission_kind::Caravan_Commune_Center_Job: return "Caravan_Commune_Center_Job";
case mission_kind::Camp_Distribute_Food: return "Camp_Distribute_Food";
case mission_kind::Camp_Determine_Leadership: return "Camp_Determine_Leadership";
case mission_kind::Camp_Have_Meal: return "Camp_Have_Meal";
case mission_kind::Camp_Hide_Mission: return "Camp_Hide_Mission";
case mission_kind::Camp_Reveal_Mission: return "Camp_Reveal_Mission";
case mission_kind::Camp_Assign_Jobs: return "Camp_Assign_Jobs";
Expand Down Expand Up @@ -230,6 +231,10 @@ static const std::array < miss_data, Camp_Harvest + 1 > miss_info = { {
"Camp_Determine_Leadership",
no_translation( "" )
},
{
"Camp_Have_Meal",
no_translation( "" )
},
{
"Hide_Mission",
no_translation( "" )
Expand Down Expand Up @@ -1210,6 +1215,7 @@ bool talk_function::handle_outpost_mission( const mission_entry &cur_key, npc &p

case Camp_Distribute_Food:
case Camp_Determine_Leadership:
case Camp_Have_Meal:
case Camp_Hide_Mission:
case Camp_Reveal_Mission:
case Camp_Assign_Jobs:
Expand Down
9 changes: 5 additions & 4 deletions src/mission_companion.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,11 @@ enum mission_kind : int {
Caravan_Commune_Center_Job,

// Faction camp tasks
Camp_Distribute_Food, // Direct action, not serialized
Camp_Determine_Leadership,
Camp_Hide_Mission, // Direct action, not serialized
Camp_Reveal_Mission, // Direct action, not serialized
Camp_Distribute_Food, // Direct action, not serialized
Camp_Determine_Leadership, // Direct action, not serialized
Camp_Have_Meal, // Direct action, not serialized
Camp_Hide_Mission, // Direct action, not serialized
Camp_Reveal_Mission, // Direct action, not serialized
Camp_Assign_Jobs,
Camp_Assign_Workers,
Camp_Abandon,
Expand Down
5 changes: 5 additions & 0 deletions src/vitamin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,11 @@ float vitamin::RDA_to_default( int percent ) const
return ( 24_hours / rate_ ) * ( static_cast<float>( percent ) / 100.0f );
}

int vitamin::units_absorption_per_day() const
{
return ( 24_hours / rate_ );
}

int vitamin::units_from_mass( vitamin_units::mass val ) const
{
if( !weight_per_unit.has_value() ) {
Expand Down
3 changes: 3 additions & 0 deletions src/vitamin.h
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ class vitamin
*/
float RDA_to_default( int percent ) const;

/** Returns how many of this vitamin (in units) can be absorbed in one day */
int units_absorption_per_day() const;

int units_from_mass( vitamin_units::mass val ) const;
// First is value, second is units (g, mg, etc)
std::pair<std::string, std::string> mass_str_from_units( int units ) const;
Expand Down

0 comments on commit 98de57e

Please sign in to comment.