Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auto-eat sorts by spoilage #2402

Merged
merged 10 commits into from
Jun 5, 2023
12 changes: 11 additions & 1 deletion src/activity_handlers.h
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,17 @@ void activity_on_turn_move_loot( player_activity &act, player &p );
bool generic_multi_activity_handler( player_activity &act, player &p, bool check_only = false );
void activity_on_turn_fetch( player_activity &, player *p );
void activity_on_turn_wear( player_activity &act, player &p );
bool find_auto_consume( player &p, bool food );

enum class consume_type : bool { FOOD, DRINK };

/**
* @brief Find an item to consume automatically
*
* @param consume_type type of item to consume
* @return true player ate food or was nauseous
* @return false player did not find anything suitable or is a npc
*/
bool find_auto_consume( player &p, const consume_type type );
void try_fuel_fire( player_activity &act, player &p, bool starting_fire = false );

enum class item_drop_reason {
Expand Down
152 changes: 95 additions & 57 deletions src/activity_item_handling.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3133,7 +3133,7 @@ static std::optional<tripoint> find_refuel_spot_trap( const std::vector<tripoint
return {};
}

bool find_auto_consume( player &p, const bool food )
bool find_auto_consume( player &p, const consume_type type )
{
// return false if there is no point searching again while the activity is still happening.
if( p.is_npc() ) {
Expand All @@ -3145,12 +3145,7 @@ bool find_auto_consume( player &p, const bool food )
const tripoint pos = p.pos();
map &here = get_map();
zone_manager &mgr = zone_manager::get_manager();
zone_type_id consume_type_zone( "" );
if( food ) {
consume_type_zone = zone_type_id( "AUTO_EAT" );
} else {
consume_type_zone = zone_type_id( "AUTO_DRINK" );
}
const zone_type_id consume_type_zone( type == consume_type::FOOD ? "AUTO_EAT" : "AUTO_DRINK" );
if( here.check_vehicle_zones( g->get_levz() ) ) {
mgr.cache_vzones();
}
Expand All @@ -3159,73 +3154,116 @@ bool find_auto_consume( player &p, const bool food )
if( dest_set.empty() ) {
return false;
}

const auto ok_to_consume = [&p, type]( item & it ) -> bool {
item &comest = p.get_consumable_from( it );
/* not food. */
if( comest.is_null() || comest.is_craft() || !comest.is_food() )
{
return false;
}
/* not enjoyable. */
if( p.fun_for( comest ).first < -5 )
{
return false;
}
/* cannot consume. */
if( !p.can_consume( comest ) )
{
return false;
}
/* wont eat, e.g cannibal */
if( !p.will_eat( comest, false ).success() )
{
return false;
}
/* not ours */
if( !it.is_owned_by( p, true ) )
{
return false;
}
/* not quenching enough */
if( type == consume_type::DRINK && comest.get_comestible()->quench < 15 )
{
return false;
}
/* Unsafe to drink or eat */
if( comest.has_flag( flag_UNSAFE_CONSUME ) )
{
return false;
}
return true;
};

struct {
item *min_shelf_life = nullptr;
tripoint loc = tripoint_min;
item_location item_loc = item_location::nowhere;

bool longer_life_than( item &it ) {
return !min_shelf_life || it.spoilage_sort_order() < min_shelf_life->spoilage_sort_order();
};
} current;

const auto should_skip = [&]( item & it ) {
return !ok_to_consume( it ) || !current.longer_life_than( it );
};

for( const tripoint loc : dest_set ) {
if( loc.z != p.pos().z ) {
continue;
}

const optional_vpart_position vp = here.veh_at( g->m.getlocal( loc ) );
std::vector<item *> items_here;
if( vp ) {
vehicle &veh = vp->vehicle();
int index = veh.part_with_feature( vp->part_index(), "CARGO", false );
if( index >= 0 ) {
vehicle_stack vehitems = veh.get_items( index );
for( item &it : vehitems ) {
items_here.push_back( &it );
const int index = veh.part_with_feature( vp->part_index(), "CARGO", false );
if( index < 0 ) {
continue;
}
/**
* TODO: when we get to use ranges library, current should be replaced with:
*
* const auto shortest = vehitems | filter_view(ok_to_consume) | max_element(spoilage_sort_order)
*
* rationale:
* 1. much more readable (mandatory FP shilling)
* 2. filter_view does not create a new container (it's a view), so it's performant
*
* @see https://en.cppreference.com/w/cpp/ranges/filter_view
* @see https://en.cppreference.com/w/cpp/algorithm/ranges/max_element
*/
vehicle_stack vehitems = veh.get_items( index );
for( item &it : vehitems ) {
if( should_skip( it ) ) {
continue;
}
current = { &it, loc, item_location( vehicle_cursor( vp->vehicle(), vp->part_index() ), &p.get_consumable_from( it ) ) };
}
} else {
map_stack mapitems = here.i_at( here.getlocal( loc ) );
for( item &it : mapitems ) {
items_here.push_back( &it );
if( should_skip( it ) ) {
continue;
}
current = { &it, loc, item_location( map_cursor( here.getlocal( loc ) ), &p.get_consumable_from( it ) ) };
}
}
}
if( !current.min_shelf_life ) {
return false;
}

for( item *it : items_here ) {
item &comest = p.get_consumable_from( *it );
if( comest.is_null() || comest.is_craft() || !comest.is_food() ||
p.fun_for( comest ).first < -5 ) {
// not good eatings.
continue;
}
if( !p.can_consume( *it ) ) {
continue;
}
if( !p.will_eat( comest, false ).success() ) {
// wont like it, cannibal meat etc
continue;
}
if( !it->is_owned_by( p, true ) ) {
// it aint ours.
continue;
}
if( !food && comest.get_comestible()->quench < 15 ) {
// not quenching enough
continue;
}
if( comest.has_flag( flag_UNSAFE_CONSUME ) ) {
// Unsafe to drink or eat
continue;
}
// actually eat
const auto cost = pickup::cost_to_move_item( p, *current.min_shelf_life );
const auto dist = std::max( rl_dist( p.pos(), here.getlocal( current.loc ) ), 1 );
p.mod_moves( -cost * dist );

p.mod_moves( -pickup::cost_to_move_item( p, *it ) * std::max( rl_dist( p.pos(),
here.getlocal( loc ) ), 1 ) );
item_location item_loc;
if( vp ) {
item_loc = item_location( vehicle_cursor( vp->vehicle(), vp->part_index() ), &comest );
} else {
item_loc = item_location( map_cursor( here.getlocal( loc ) ), &comest );
}
avatar_action::eat( g->u, item_loc );
// eat() may have removed the item, so check its still there.
if( item_loc.get_item() && item_loc->is_container() ) {
item_loc->on_contents_changed();
}
return true;
}
avatar_action::eat( g->u, current.item_loc );
// eat() may have removed the item, so check its still there.
if( current.item_loc.get_item() && current.item_loc->is_container() ) {
current.item_loc->on_contents_changed();
}
return false;
return true;
}

void try_fuel_fire( player_activity &act, player &p, const bool starting_fire )
Expand Down
4 changes: 2 additions & 2 deletions src/player_activity.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -282,12 +282,12 @@ void player_activity::do_turn( player &p )
}
if( *this && !p.is_npc() && type->valid_auto_needs() && !no_food_nearby_for_auto_consume ) {
if( p.get_kcal_percent() < 0.95f ) {
if( !find_auto_consume( p, true ) ) {
if( !find_auto_consume( p, consume_type::FOOD ) ) {
no_food_nearby_for_auto_consume = true;
}
}
if( p.get_thirst() > thirst_levels::thirsty && !no_drink_nearby_for_auto_consume ) {
if( !find_auto_consume( p, false ) ) {
if( !find_auto_consume( p, consume_type::DRINK ) ) {
no_drink_nearby_for_auto_consume = true;
}
}
Expand Down
171 changes: 171 additions & 0 deletions tests/find_auto_consume_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
#include "avatar.h"
#include "character.h"
#include "clzones.h"
#include "itype.h"
#include "player.h"
#include "map.h"
#include "item.h"
#include "activity_handlers.h"
#include "avatar_action.h"
#include "item_location.h"
#include "pickup.h"
#include "calendar.h"
#include "player_helpers.h"
#include "state_helpers.h"
#include "vehicle.h"
#include "type_id.h"

#include "catch/catch.hpp"

/** food items are counted by charges */
static auto get_single_food_item( const tripoint &pos ) -> const item &
{
map &here = get_map();
const auto &items = here.i_at( pos );
CHECK( items.size() == 1 );

return *items.begin();
}

TEST_CASE( "auto_consume_priority", "[auto_consume][food][zone]" )
{
clear_all_state();

map &here = get_map();
auto &zmgr = zone_manager::get_manager();

constexpr auto zone_origin = tripoint{ 60, 60, 0 };
tripoint zone_origin_absolute = here.getabs( zone_origin );
constexpr auto zone_size = tripoint{ 6, 6, 0 };

avatar &you = get_avatar();
you.setpos( zone_origin );

static auto create_zone = [&]( const std::string & name ) -> void {
zmgr.add( name, zone_type_id( name ),
faction_id( "your_followers" ), false, true,
zone_origin_absolute - zone_size,
zone_origin_absolute + zone_size );
};

static auto place_items = [&]( const std::vector<std::pair<item, tripoint>> &item_pairs ) -> void {
for( const auto &[ item, pos ] : item_pairs )
{
here.add_item_or_charges( pos, item );
}
};

static const auto auto_consume = [&]( consume_type consume ) {
return [&you, consume]( int count ) -> bool {
bool ok = true;
for( int i = 0; i < count; i++ )
{
ok &= find_auto_consume( you, consume );
}
return ok;
};
};

using PosCounts = std::vector<std::pair<tripoint, int>>;

SECTION( "auto_eat" ) {
static const auto check_item_count =
[&]( const PosCounts & expected ) -> void {
for( const auto&[ pos, count ] : expected )
{
if( count == 0 ) {
INFO( "expected empty at " << pos );
CHECK( here.i_at( pos ).empty() );
} else {
INFO( "expected " << count << " at " << pos );
CHECK( get_single_food_item( pos ).count() == count );
}
}
};

static const auto auto_eat = auto_consume( consume_type::FOOD );

clear_avatar();
you.set_stored_kcal( 1000 );

create_zone( "AUTO_EAT" );

auto meat = item{ "meat_cooked", calendar::turn, 5 }; // shelf life: 2 days
auto meat_pos = zone_origin;
auto nuts = item{ "pine_nuts", calendar::turn, 5 }; // shelf life: 3 seasons
auto nuts_pos = zone_origin + tripoint( 1, 0, 0 );
auto hardtack = item{ "hardtack", calendar::turn, 5 }; // shelf life: 6 years
auto hardtack_pos = zone_origin + tripoint( 2, 0, 0 );

place_items( { { meat, meat_pos }, { nuts, nuts_pos }, { hardtack, hardtack_pos } } );

CHECK( auto_eat( 5 ) );
check_item_count( { { meat_pos, 0 }, { nuts_pos, 5 }, { hardtack_pos, 5 } } );
CHECK( auto_eat( 5 ) );
check_item_count( { { meat_pos, 0 }, { nuts_pos, 0 }, { hardtack_pos, 5 } } );
CHECK( auto_eat( 5 ) );
check_item_count( { { meat_pos, 0 }, { nuts_pos, 0 }, { hardtack_pos, 0 } } );

// check that the player has consumed the food
CHECK( you.stomach.get_calories() > 1000 );
}

SECTION( "auto_drink" ) {
static const auto check_drink_amount =
[&]( const PosCounts & expected ) -> void {
for( const auto&[ pos, count ] : expected )
{
auto jar = get_single_food_item( pos );
auto &contained = jar.get_contained();
INFO( contained.tname() << " has " << contained.count() << " charges" );
if( count == 0 ) {
CHECK( contained.is_null() );
} else {
CHECK( contained.count() == count );
}
}
};

static const auto auto_drink = auto_consume( consume_type::DRINK );

create_zone( "AUTO_DRINK" );

auto jar = itype_id{"jar_3l_glass"};
auto water = item( "water_clean" );
auto water_bottle = water.in_container( jar );
auto water_pos = zone_origin;
auto orange = item( "oj" ); // 5 days
auto orange_bottle = orange.in_container( jar );
auto orange_pos = zone_origin + tripoint( 1, 0, 0 );
auto cocoa = item( "hot_chocolate" ); // 1 day
auto cocoa_bottle = cocoa.in_container( jar );
auto cocoa_pos = zone_origin + tripoint( 2, 0, 0 );

place_items( { { water_bottle, water_pos }, { orange_bottle, orange_pos }, { cocoa_bottle, cocoa_pos } } );

SECTION( "full character won't drink drink with calories" ) {
clear_avatar();
you.set_stored_kcal( you.max_stored_kcal() );
you.set_thirst( 700 );

check_drink_amount( { { water_pos, 12 }, { orange_pos, 12 }, { cocoa_pos, 12 } } );
CHECK( auto_drink( 6 ) );
check_drink_amount( { { water_pos, 6 }, { orange_pos, 12 }, { cocoa_pos, 12 } } );

CHECK( you.get_thirst() < 700 );
}

SECTION( "hungry character will drink drink with calories" ) {
clear_avatar();
you.set_thirst( 700 );
you.set_stored_kcal( 1000 );

CHECK( auto_drink( 12 ) );
INFO( "only cocoa should be consumed" );
// FIXME: can't figure out why water is replenished, but it is
check_drink_amount( { { water_pos, 12 }, { orange_pos, 12 }, { cocoa_pos, 0 } } );

CHECK( you.get_thirst() < 700 );
}
}
}