Skip to content

Commit

Permalink
Fixes activity zone sorting for comestibles in containers.
Browse files Browse the repository at this point in the history
  • Loading branch information
Squishums committed Nov 16, 2020
1 parent 938a274 commit 81162c3
Show file tree
Hide file tree
Showing 7 changed files with 308 additions and 4 deletions.
23 changes: 23 additions & 0 deletions data/mods/TEST_DATA/items.json
Original file line number Diff line number Diff line change
Expand Up @@ -1269,6 +1269,29 @@
"loudness_modifier": -30,
"flags": [ "DISABLE_SIGHTS", "CONSUMABLE", "REACH_ATTACK" ]
},
{
"type": "COMESTIBLE",
"id": "test_milk",
"name": { "str_sp": "intolerant milk" },
"weight": "258 g",
"color": "white",
"spoils_in": "1 d",
"container": "jug_plastic",
"comestible_type": "DRINK",
"symbol": "~",
"quench": 25,
"healthy": 1,
"calories": 132,
"description": "You can only push a drink so far before it spoils.",
"price": 38,
"price_postapoc": 50,
"material": [ "milk" ],
"volume": "250 ml",
"phase": "liquid",
"flags": [ "EATEN_COLD" ],
"vitamins": [ [ "vitA", 10 ], [ "vitB", 6 ], [ "vitC", 4 ], [ "calcium", 25 ] ],
"fun": 1
},
{
"type": "COMESTIBLE",
"id": "test_brew_wine",
Expand Down
31 changes: 27 additions & 4 deletions src/clzones.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -839,17 +839,40 @@ zone_type_id zone_manager::get_near_zone_type_for_item( const item &it,
}

if( cat.get_id() == item_category_food ) {
// skip food without comestible, like MREs
if( const item *it_food = it.get_food() ) {
const item *it_food = nullptr;
bool perishable = false;
// Look for food, and whether any contents which will spoil if left out.
// Food crafts and food without comestible, like MREs, will fall down to LOOT_FOOD.
it.visit_items( [&]( const item * node, const item * parent ) {
if( node && node->is_food() ) {
it_food = node;

if( node->goes_bad() ) {
float spoil_multiplier = 1.0f;
if( parent ) {
const item_pocket *parent_pocket = parent->contained_where( *node );
if( parent_pocket ) {
spoil_multiplier = parent_pocket->spoil_multiplier();
}
}
if( spoil_multiplier > 0.0f ) {
perishable = true;
}
}
}
return VisitResponse::NEXT;
} );

if( it_food != nullptr ) {
if( it_food->get_comestible()->comesttype == "DRINK" ) {
if( it_food->goes_bad() && has_near( zone_type_id( "LOOT_PDRINK" ), where, range ) ) {
if( perishable && has_near( zone_type_id( "LOOT_PDRINK" ), where, range ) ) {
return zone_type_id( "LOOT_PDRINK" );
} else if( has_near( zone_type_id( "LOOT_DRINK" ), where, range ) ) {
return zone_type_id( "LOOT_DRINK" );
}
}

if( it_food->goes_bad() && has_near( zone_type_id( "LOOT_PFOOD" ), where, range ) ) {
if( perishable && has_near( zone_type_id( "LOOT_PFOOD" ), where, range ) ) {
return zone_type_id( "LOOT_PFOOD" );
}
}
Expand Down
5 changes: 5 additions & 0 deletions src/item.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6821,6 +6821,11 @@ item_pocket *item::contained_where( const item &contained )
return contents.contained_where( contained );
}

const item_pocket *item::contained_where(const item &contained) const
{
return const_cast<item *>(this)->contained_where(contained);
}

bool item::is_watertight_container() const
{
return contents.can_contain_liquid( true );
Expand Down
1 change: 1 addition & 0 deletions src/item.h
Original file line number Diff line number Diff line change
Expand Up @@ -719,6 +719,7 @@ class item : public visitable<item>
// for pocket update stuff, which pocket is @contained in?
// returns a nullptr if the item is not contaiend, and prints a debug message
item_pocket *contained_where( const item &contained );
const item_pocket *contained_where(const item &contained) const;
/** Whether this is a container which can be used to store liquids. */
bool is_watertight_container() const;
/** Whether this item has no contents at all. */
Expand Down
236 changes: 236 additions & 0 deletions tests/clzones_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
#include "catch/catch.hpp"

#include "clzones.h"
#include "game_constants.h"
#include "item.h"
#include "item_category.h"
#include "map.h"
#include "map_helpers.h"

static const zone_type_id zone_type_LOOT_UNSORTED( "LOOT_UNSORTED" );
static const zone_type_id zone_type_LOOT_FOOD( "LOOT_FOOD" );
static const zone_type_id zone_type_LOOT_PFOOD( "LOOT_PFOOD" );
static const zone_type_id zone_type_LOOT_DRINK( "LOOT_DRINK" );
static const zone_type_id zone_type_LOOT_PDRINK( "LOOT_PDRINK" );

void create_tile_zone( const std::string &name, const zone_type_id &zone_type, tripoint pos )
{
zone_manager &zm = zone_manager::get_manager();
zm.add( name, zone_type, faction_id( "your_followers" ), false, true, pos, pos );
}

// Comestibles sorting is a bit awkward. Unlike other loot, they're almost
// always inside of a container, and their sort zone changes based on their
// shelf life and whether the container prevents rotting.
TEST_CASE( "zone sorting comestibles ", "[zones][items][food][activities]" )
{
clear_map();
zone_manager &zm = zone_manager::get_manager();
map &here = get_map();

const tripoint origin_pos = tripoint( 0, 0, 0 );
create_tile_zone( "Food", zone_type_LOOT_FOOD, tripoint( 1, 0, 0 ) );
create_tile_zone( "Drink", zone_type_LOOT_DRINK, tripoint( 2, 0, 0 ) );

SECTION( "without perishable zones" ) {
GIVEN( "a non-perishable food" ) {
item nonperishable_food( "test_bitter_almond" );
REQUIRE_FALSE( nonperishable_food.goes_bad() );

WHEN( "sorting without a container" ) {
THEN( "should put in the food zone" ) {
CHECK( zm.get_near_zone_type_for_item( nonperishable_food, origin_pos ) == zone_type_LOOT_FOOD );
}
}
}

GIVEN( "a non-perishable drink" ) {
item nonperishable_drink( "test_wine" );
REQUIRE_FALSE( nonperishable_drink.goes_bad() );

WHEN( "sorting without a container" ) {
THEN( "should put in the drink zone" ) {
CHECK( zm.get_near_zone_type_for_item( nonperishable_drink, origin_pos ) == zone_type_LOOT_DRINK );
}
}
}

GIVEN( "a perishable food" ) {
item perishable_food( "test_apple" );
REQUIRE( perishable_food.goes_bad() );

WHEN( "sorting without a container" ) {
THEN( "should put in the food zone" ) {
CHECK( zm.get_near_zone_type_for_item( perishable_food, origin_pos ) == zone_type_LOOT_FOOD );
}
}
}

GIVEN( "a perishable drink" ) {
item perishable_drink( "test_milk" );
REQUIRE( perishable_drink.goes_bad() );

WHEN( "sorting without a container" ) {
THEN( "should put in the drink zone" ) {
CHECK( zm.get_near_zone_type_for_item( perishable_drink, origin_pos ) == zone_type_LOOT_DRINK );
}
}
}
}

SECTION( "with perishable zones" ) {
create_tile_zone( "PFood", zone_type_LOOT_PFOOD, tripoint( 3, 0, 0 ) );
create_tile_zone( "PDrink", zone_type_LOOT_PDRINK, tripoint( 4, 0, 0 ) );

GIVEN( "a non-perishable food" ) {
item nonperishable_food( "test_bitter_almond" );
REQUIRE_FALSE( nonperishable_food.goes_bad() );

WHEN( "sorting without a container" ) {
THEN( "should put in the food zone" ) {
CHECK( zm.get_near_zone_type_for_item( nonperishable_food, origin_pos ) == zone_type_LOOT_FOOD );
}
}

WHEN( "sorting within an unsealed container" ) {
item container( "test_watertight_open_sealed_container_250ml" );
REQUIRE( container.put_in( nonperishable_food, item_pocket::pocket_type::CONTAINER ).success() );
REQUIRE( container.contents.get_sealed_summary() == item_contents::sealed_summary::unsealed );

THEN( "should put in the food zone" ) {
CHECK( zm.get_near_zone_type_for_item( container, origin_pos ) == zone_type_LOOT_FOOD );
}
}

WHEN( "sorting within a sealed container" ) {
item container( "test_watertight_open_sealed_container_250ml" );
REQUIRE( container.put_in( nonperishable_food, item_pocket::pocket_type::CONTAINER ).success() );
REQUIRE( container.seal() );
REQUIRE( container.contents.get_all_contained_pockets().value().front()->spoil_multiplier() ==
0.0f );
REQUIRE( container.contents.get_sealed_summary() == item_contents::sealed_summary::all_sealed );

THEN( "should put in the food zone" ) {
CHECK( zm.get_near_zone_type_for_item( container, origin_pos ) == zone_type_LOOT_FOOD );
}
}
}

GIVEN( "a non-perishable drink" ) {
item nonperishable_drink( "test_wine" );
REQUIRE_FALSE( nonperishable_drink.goes_bad() );

WHEN( "sorting without a container" ) {
THEN( "should put in the drink zone" ) {
CHECK( zm.get_near_zone_type_for_item( nonperishable_drink, origin_pos ) == zone_type_LOOT_DRINK );
}
}

WHEN( "sorting within an unsealed container" ) {
item container( "test_watertight_open_sealed_container_250ml" );
REQUIRE( container.put_in( nonperishable_drink, item_pocket::pocket_type::CONTAINER ).success() );
REQUIRE( container.contents.get_sealed_summary() == item_contents::sealed_summary::unsealed );

THEN( "should put in the drink zone" ) {
CHECK( zm.get_near_zone_type_for_item( container, origin_pos ) == zone_type_LOOT_DRINK );
}
}

WHEN( "sorting within a sealed container" ) {
item container( "test_watertight_open_sealed_container_250ml" );
REQUIRE( container.put_in( nonperishable_drink, item_pocket::pocket_type::CONTAINER ).success() );
REQUIRE( container.seal() );
REQUIRE( container.contents.get_all_contained_pockets().value().front()->spoil_multiplier() ==
0.0f );
REQUIRE( container.contents.get_sealed_summary() == item_contents::sealed_summary::all_sealed );

THEN( "should put in the drink zone" ) {
CHECK( zm.get_near_zone_type_for_item( container, origin_pos ) == zone_type_LOOT_DRINK );
}
}
}


GIVEN( "a perishable food" ) {
item perishable_food( "test_apple" );
REQUIRE( perishable_food.goes_bad() );

WHEN( "sorting without a container" ) {
THEN( "should put in the perishable food zone" ) {
CHECK( zm.get_near_zone_type_for_item( perishable_food, origin_pos ) == zone_type_LOOT_PFOOD );
}
}

WHEN( "sorting within an unsealed container" ) {
item container( "test_watertight_open_sealed_container_250ml" );
REQUIRE( container.put_in( perishable_food, item_pocket::pocket_type::CONTAINER ).success() );
REQUIRE( container.contents.get_sealed_summary() == item_contents::sealed_summary::unsealed );

THEN( "should put in the perishable food zone" ) {
CHECK( zm.get_near_zone_type_for_item( container, origin_pos ) == zone_type_LOOT_PFOOD );
}
}

WHEN( "sorting within a sealed container" ) {
item container( "test_watertight_open_sealed_container_250ml" );
REQUIRE( container.put_in( perishable_food, item_pocket::pocket_type::CONTAINER ).success() );
REQUIRE( container.seal() );
REQUIRE( container.contents.get_all_contained_pockets().value().front()->spoil_multiplier() ==
0.0f );
REQUIRE( container.contents.get_sealed_summary() == item_contents::sealed_summary::all_sealed );

THEN( "should put in the food zone" ) {
CHECK( zm.get_near_zone_type_for_item( container, origin_pos ) == zone_type_LOOT_FOOD );
}
}
}

GIVEN( "a perishable drink" ) {
item perishable_drink( "test_milk" );
REQUIRE( perishable_drink.goes_bad() );

WHEN( "sorting without a container" ) {
THEN( "should put in the perishable drink zone" ) {
CHECK( zm.get_near_zone_type_for_item( perishable_drink, origin_pos ) == zone_type_LOOT_PDRINK );
}
}

WHEN( "sorting within an unsealed container" ) {
item container( "test_watertight_open_sealed_container_250ml" );
REQUIRE( container.put_in( perishable_drink, item_pocket::pocket_type::CONTAINER ).success() );
REQUIRE( container.contents.get_sealed_summary() == item_contents::sealed_summary::unsealed );

THEN( "should put in the perishable drink zone" ) {
CHECK( zm.get_near_zone_type_for_item( container, origin_pos ) == zone_type_LOOT_PDRINK );
}
}

WHEN( "sorting within a sealed container" ) {
item container( "test_watertight_open_sealed_container_250ml" );
REQUIRE( container.put_in( perishable_drink, item_pocket::pocket_type::CONTAINER ).success() );
REQUIRE( container.seal() );
REQUIRE( container.contents.get_all_contained_pockets().value().front()->spoil_multiplier() ==
0.0f );
REQUIRE( container.contents.get_sealed_summary() == item_contents::sealed_summary::all_sealed );

THEN( "should put in the drink zone" ) {
CHECK( zm.get_near_zone_type_for_item( container, origin_pos ) == zone_type_LOOT_DRINK );
}
}
}


// MREs are under the food category but are not directly edible.
GIVEN( "a non-comestible food" ) {
item noncomestible_food( "mre_dessert" );
REQUIRE( noncomestible_food.get_category_shallow().get_id() == item_category_id( "food" ) );
REQUIRE_FALSE( noncomestible_food.is_comestible() );

WHEN( "sorting" ) {
THEN( "should put in the food zone" ) {
CHECK( zm.get_near_zone_type_for_item( noncomestible_food, origin_pos ) == zone_type_LOOT_FOOD );
}
}
}
}
}
15 changes: 15 additions & 0 deletions tests/map_helpers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include "npc.h"
#include "point.h"
#include "type_id.h"
#include "clzones.h"

// Remove all vehicles from the map
void clear_vehicles()
Expand Down Expand Up @@ -85,13 +86,27 @@ void clear_items( const int zlevel )
}
}

void clear_zones()
{
zone_manager &zm = zone_manager::get_manager();
for( auto zone_ref : zm.get_zones( faction_id( "your_followers" ) ) ) {
if (!zone_ref.get().get_is_vehicle()) {
// Trying to delete vehicle zones fails with a message that the zone isn't loaded.
// Don't need it right now and the errors spam up the test output, so skip.
continue;
}
zm.remove(zone_ref.get());
}
}

void clear_map()
{
// Clearing all z-levels is rather slow, so just clear the ones I know the
// tests use for now.
for( int z = -2; z <= 0; ++z ) {
clear_fields( z );
}
clear_zones();
wipe_map_terrain();
clear_npcs();
clear_creatures();
Expand Down
1 change: 1 addition & 0 deletions tests/map_helpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ void clear_creatures();
void clear_npcs();
void clear_fields( int zlevel );
void clear_items( int zlevel );
void clear_zones();
void clear_map();
void clear_map_and_put_player_underground();
monster &spawn_test_monster( const std::string &monster_type, const tripoint &start );
Expand Down

0 comments on commit 81162c3

Please sign in to comment.