Skip to content

Commit

Permalink
Detect and warn about crafting with rotten food
Browse files Browse the repository at this point in the history
To deal with the long-standing issue of accidentally using rotting food
in crafting (especially when crafting a large batch) and thus wasting
time and materials.

Allow a recipe filter to exclude rotten components.

Use this new feature when selecting items for crafting to prefer
non-rotten ones.  When non-rotten ones do not suffice, warn the player
of this fact before proceeding.

In the crafting GUI, mark crafts in brown when they can only be crafted
with rotten ingredients.

This only catches the case where the ingredients are rotten when you
start the craft.  There is also the situation where things start
non-rotten, but become rotten while you craft.  This change does not
attempt to handle that, but it could now be handled by a localized
change in recipe::get_component_filter.
  • Loading branch information
jbytheway committed Jan 1, 2020
1 parent 9eb7975 commit aa5b77c
Show file tree
Hide file tree
Showing 7 changed files with 105 additions and 57 deletions.
22 changes: 16 additions & 6 deletions src/craft_command.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ void craft_command::execute( const tripoint &new_loc )

if( need_selections ) {
if( !crafter->can_make( rec, batch_size ) ) {
if( crafter->can_start_craft( rec, batch_size ) ) {
if( crafter->can_start_craft( rec, recipe_filter_flags::none, batch_size ) ) {
if( !query_yn( _( "You don't have enough charges to complete the %s.\n"
"Start crafting anyway?" ), rec->result_name() ) ) {
return;
Expand All @@ -133,13 +133,23 @@ void craft_command::execute( const tripoint &new_loc )
}
}

flags = recipe_filter_flags::no_rotten;

if( !crafter->can_start_craft( rec, flags, batch_size ) ) {
if( !query_yn( _( "This craft will use rotten components.\n"
"Start crafting anyway?" ) ) ) {
return;
}
flags = recipe_filter_flags::none;
}

item_selections.clear();
const auto needs = rec->requirements();
const auto filter = rec->get_component_filter();
const auto filter = rec->get_component_filter( flags );

for( const auto &it : needs.get_components() ) {
comp_selection<item_comp> is = crafter->select_item_component( it, batch_size, map_inv, true,
filter );
comp_selection<item_comp> is =
crafter->select_item_component( it, batch_size, map_inv, true, filter );
if( is.use_from == cancel ) {
return;
}
Expand Down Expand Up @@ -228,7 +238,7 @@ item craft_command::create_in_progress_craft()
return item();
}

const auto filter = rec->get_component_filter();
const auto filter = rec->get_component_filter( flags );

for( const auto &it : item_selections ) {
std::list<item> tmp = crafter->consume_items( it, batch_size, filter );
Expand Down Expand Up @@ -273,7 +283,7 @@ std::vector<comp_selection<item_comp>> craft_command::check_item_components_miss
{
std::vector<comp_selection<item_comp>> missing;

const auto filter = rec->get_component_filter();
const auto filter = rec->get_component_filter( flags );

for( const auto &item_sel : item_selections ) {
itype_id type = item_sel.comp.type;
Expand Down
3 changes: 3 additions & 0 deletions src/craft_command.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <string>

#include "point.h"
#include "recipe.h"
#include "requirements.h"

class inventory;
Expand Down Expand Up @@ -96,6 +97,8 @@ class craft_command
// This is mainly here for maintainability reasons.
player *crafter;

recipe_filter_flags flags = recipe_filter_flags::none;

// Location of the workbench to place the item on
// zero_tripoint indicates crafting without a workbench
tripoint loc = tripoint_zero;
Expand Down
19 changes: 16 additions & 3 deletions src/crafting.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -482,7 +482,7 @@ bool player::can_make( const recipe *r, int batch_size )
batch_size );
}

bool player::can_start_craft( const recipe *rec, int batch_size )
bool player::can_start_craft( const recipe *rec, recipe_filter_flags flags, int batch_size )
{
if( !rec ) {
return false;
Expand Down Expand Up @@ -525,7 +525,8 @@ bool player::can_start_craft( const recipe *rec, int batch_size )
rec->requirements().get_qualities(),
adjusted_comp_reqs );

return start_reqs.can_make_with_inventory( crafting_inventory(), rec->get_component_filter() );
return start_reqs.can_make_with_inventory( crafting_inventory(),
rec->get_component_filter( flags ) );
}

const inventory &player::crafting_inventory( bool clear_path )
Expand Down Expand Up @@ -1254,7 +1255,9 @@ bool player::can_continue_craft( item &craft )
// Avoid building an inventory from the map if we don't have to, as it is expensive
if( !continue_reqs.is_empty() ) {

const std::function<bool( const item & )> filter = rec.get_component_filter();
std::function<bool( const item & )> filter = rec.get_component_filter();
const std::function<bool( const item & )> no_rotten_filter =
rec.get_component_filter( recipe_filter_flags::no_rotten );
// continue_reqs are for all batches at once
const int batch_size = 1;

Expand All @@ -1273,6 +1276,16 @@ bool player::can_continue_craft( item &craft )
return false;
}

if( continue_reqs.can_make_with_inventory( crafting_inventory(), no_rotten_filter,
batch_size ) ) {
filter = no_rotten_filter;
} else {
if( !query_yn( _( "Some components required to continue are rotten.\n"
"Continue crafting anyway?" ) ) ) {
return false;
}
}

inventory map_inv;
map_inv.form_from_map( pos(), PICKUP_RANGE, this );

Expand Down
92 changes: 48 additions & 44 deletions src/crafting_gui.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,26 @@ const recipe *select_crafting_recipe( int &batch_size )
list_circularizer<std::string> tab( craft_cat_list );
list_circularizer<std::string> subtab( craft_subcat_list[tab.cur()] );
std::vector<const recipe *> current;
std::vector<bool> available;

struct availability {
availability( const recipe *r, int batch_size = 1 ) :
can_craft( g->u.can_start_craft( r, recipe_filter_flags::none, batch_size ) ),
can_craft_non_rotten( g->u.can_start_craft( r, recipe_filter_flags::no_rotten,
batch_size ) )
{}
bool can_craft;
bool can_craft_non_rotten;

nc_color selected_color() const {
return can_craft ? can_craft_non_rotten ? h_white : h_brown : h_dark_gray;
}

nc_color color() const {
return can_craft ? can_craft_non_rotten ? c_white : c_brown : c_dark_gray;
}
};

std::vector<availability> available;
const int componentPrintHeight = dataHeight - tailHeight - 1;
//preserves component color printout between mode rotations
nc_color rotated_color = c_white;
Expand Down Expand Up @@ -237,7 +256,7 @@ const recipe *select_crafting_recipe( int &batch_size )
std::string filterstring;

const auto &available_recipes = g->u.get_available_recipes( crafting_inv, &helpers );
std::map<const recipe *, bool> availability_cache;
std::map<const recipe *, availability> availability_cache;

do {
if( redraw ) {
Expand All @@ -264,7 +283,7 @@ const recipe *select_crafting_recipe( int &batch_size )
current.clear();
for( int i = 1; i <= 20; i++ ) {
current.push_back( chosen );
available.push_back( g->u.can_start_craft( chosen, i ) );
available.push_back( availability( chosen, i ) );
}
} else {
std::vector<const recipe *> picking;
Expand Down Expand Up @@ -363,23 +382,26 @@ const recipe *select_crafting_recipe( int &batch_size )
// cache recipe availability on first display
for( const auto e : current ) {
if( !availability_cache.count( e ) ) {
availability_cache.emplace( e, g->u.can_start_craft( e ) );
availability_cache.emplace( e, availability( e ) );
}
}

if( subtab.cur() != "CSC_*_RECENT" ) {
std::stable_sort( current.begin(), current.end(), []( const recipe * a, const recipe * b ) {
std::stable_sort( current.begin(), current.end(),
[]( const recipe * a, const recipe * b ) {
return b->difficulty < a->difficulty;
} );

std::stable_sort( current.begin(), current.end(), [&]( const recipe * a, const recipe * b ) {
return availability_cache[a] && !availability_cache[b];
std::stable_sort( current.begin(), current.end(),
[&]( const recipe * a, const recipe * b ) {
return availability_cache.at( a ).can_craft &&
!availability_cache.at( b ).can_craft;
} );
}

std::transform( current.begin(), current.end(),
std::back_inserter( available ), [&]( const recipe * e ) {
return availability_cache[e];
return availability_cache.at( e );
} );
}

Expand Down Expand Up @@ -443,13 +465,8 @@ const recipe *select_crafting_recipe( int &batch_size )
tmp_name = string_format( _( "%2dx %s" ), i + 1, tmp_name );
}
mvwprintz( w_data, point( 2, i - recmin ), c_dark_gray, "" ); // Clear the line
if( i == line ) {
mvwprintz( w_data, point( 2, i - recmin ), ( available[i] ? h_white : h_dark_gray ),
utf8_truncate( tmp_name, 28 ) );
} else {
mvwprintz( w_data, point( 2, i - recmin ), ( available[i] ? c_white : c_dark_gray ),
utf8_truncate( tmp_name, 28 ) );
}
nc_color col = i == line ? available[i].selected_color() : available[i].color();
mvwprintz( w_data, point( 2, i - recmin ), col, utf8_truncate( tmp_name, 28 ) );
}
} else if( line >= recmax - dataHalfLines ) {
for( int i = recmax - dataLines; i < recmax; ++i ) {
Expand All @@ -458,15 +475,9 @@ const recipe *select_crafting_recipe( int &batch_size )
tmp_name = string_format( _( "%2dx %s" ), i + 1, tmp_name );
}
mvwprintz( w_data, point( 2, dataLines + i - recmax ), c_light_gray, "" ); // Clear the line
if( i == line ) {
mvwprintz( w_data, point( 2, dataLines + i - recmax ),
( available[i] ? h_white : h_dark_gray ),
utf8_truncate( tmp_name, 28 ) );
} else {
mvwprintz( w_data, point( 2, dataLines + i - recmax ),
( available[i] ? c_white : c_dark_gray ),
utf8_truncate( tmp_name, 28 ) );
}
nc_color col = i == line ? available[i].selected_color() : available[i].color();
mvwprintz( w_data, point( 2, dataLines + i - recmax ), col,
utf8_truncate( tmp_name, 28 ) );
}
} else {
for( int i = line - dataHalfLines; i < line - dataHalfLines + dataLines; ++i ) {
Expand All @@ -475,37 +486,26 @@ const recipe *select_crafting_recipe( int &batch_size )
tmp_name = string_format( _( "%2dx %s" ), i + 1, tmp_name );
}
mvwprintz( w_data, point( 2, dataHalfLines + i - line ), c_light_gray, "" ); // Clear the line
if( i == line ) {
mvwprintz( w_data, point( 2, dataHalfLines + i - line ),
( available[i] ? h_white : h_dark_gray ),
utf8_truncate( tmp_name, 28 ) );
} else {
mvwprintz( w_data, point( 2, dataHalfLines + i - line ),
( available[i] ? c_white : c_dark_gray ),
utf8_truncate( tmp_name, 28 ) );
}
nc_color col = i == line ? available[i].selected_color() : available[i].color();
mvwprintz( w_data, point( 2, dataHalfLines + i - line ), col,
utf8_truncate( tmp_name, 28 ) );
}
}
} else {
for( size_t i = 0; i < current.size() && i < static_cast<size_t>( dataHeight ) + 1; ++i ) {
for( int i = 0; i < static_cast<int>( current.size() ) && i < dataHeight + 1; ++i ) {
std::string tmp_name = current[i]->result_name();
if( batch ) {
tmp_name = string_format( _( "%2dx %s" ), static_cast<int>( i ) + 1, tmp_name );
}
if( static_cast<int>( i ) == line ) {
mvwprintz( w_data, point( 2, i ), ( available[i] ? h_white : h_dark_gray ),
utf8_truncate( tmp_name, 28 ) );
} else {
mvwprintz( w_data, point( 2, i ), ( available[i] ? c_white : c_dark_gray ),
utf8_truncate( tmp_name, 28 ) );
tmp_name = string_format( _( "%2dx %s" ), i + 1, tmp_name );
}
nc_color col = i == line ? available[i].selected_color() : available[i].color();
mvwprintz( w_data, point( 2, i ), col, utf8_truncate( tmp_name, 28 ) );
}
}

if( !current.empty() ) {
int pane = FULL_SCREEN_WIDTH - 30 - 1;
int count = batch ? line + 1 : 1; // batch size
nc_color col = available[ line ] ? c_white : c_light_gray;
nc_color col = available[ line ].color();

const auto &req = current[ line ]->requirements();

Expand Down Expand Up @@ -599,6 +599,10 @@ const recipe *select_crafting_recipe( int &batch_size )
current[line]->has_flag( "BLIND_EASY" ) ? _( "Easy" ) :
current[line]->has_flag( "BLIND_HARD" ) ? _( "Hard" ) :
_( "Impossible" ) ) );
if( available[line].can_craft && !available[line].can_craft_non_rotten ) {
ypos += fold_and_print( w_data, point( xpos, ypos ), pane, col,
_( "<color_red>Will use rotten ingredients</color>" ) );
}
ypos += print_items( *current[line], w_data, ypos, xpos, col, batch ? line + 1 : 1 );
}

Expand Down Expand Up @@ -693,7 +697,7 @@ const recipe *select_crafting_recipe( int &batch_size )
} else if( action == "UP" ) {
line--;
} else if( action == "CONFIRM" ) {
if( available.empty() || !available[line] ) {
if( available.empty() || !available[line].can_craft ) {
popup( _( "You can't do that!" ) );
} else if( !g->u.check_eligible_containers_for_crafting( *current[line],
( batch ) ? line + 1 : 1 ) ) {
Expand Down
3 changes: 2 additions & 1 deletion src/player.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class map;
class npc;
struct pathfinding_settings;
class recipe;
enum class recipe_filter_flags : int;
struct islot_comestible;
struct itype;
class monster;
Expand Down Expand Up @@ -1045,7 +1046,7 @@ class player : public Character
* The player is not required to have enough tool charges to finish crafting, only to
* complete the first step (total / 20 + total % 20 charges)
*/
bool can_start_craft( const recipe *rec, int batch_size = 1 );
bool can_start_craft( const recipe *rec, recipe_filter_flags, int batch_size = 1 );
bool making_would_work( const recipe_id &id_to_make, int batch_size );

/**
Expand Down
9 changes: 7 additions & 2 deletions src/recipe.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -500,14 +500,19 @@ bool recipe::will_be_blacklisted() const
return any_is_blacklisted( reqs_internal ) || any_is_blacklisted( reqs_external );
}

std::function<bool( const item & )> recipe::get_component_filter() const
std::function<bool( const item & )> recipe::get_component_filter(
const recipe_filter_flags flags ) const
{
const item result = create_result();

// Disallow crafting of non-perishables with rotten components
// Make an exception for items with the ALLOW_ROTTEN flag such as seeds
const bool recipe_forbids_rotten =
result.is_food() && !result.goes_bad() && !has_flag( "ALLOW_ROTTEN" );
const bool flags_forbid_rotten =
static_cast<bool>( flags & recipe_filter_flags::no_rotten ) && result.goes_bad();
std::function<bool( const item & )> rotten_filter = return_true<item>;
if( result.is_food() && !result.goes_bad() && !has_flag( "ALLOW_ROTTEN" ) ) {
if( recipe_forbids_rotten || flags_forbid_rotten ) {
rotten_filter = []( const item & component ) {
return !component.rotten();
};
Expand Down
14 changes: 13 additions & 1 deletion src/recipe.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@ class time_duration;
using itype_id = std::string; // From itype.h
class Character;

enum class recipe_filter_flags : int {
none = 0,
no_rotten = 1,
};

inline constexpr recipe_filter_flags operator&( recipe_filter_flags l, recipe_filter_flags r )
{
return static_cast<recipe_filter_flags>(
static_cast<unsigned>( l ) & static_cast<unsigned>( r ) );
}

class recipe
{
friend class recipe_dictionary;
Expand Down Expand Up @@ -66,7 +77,8 @@ class recipe
// recipe finalization happens
bool will_be_blacklisted() const;

std::function<bool( const item & )> get_component_filter() const;
std::function<bool( const item & )> get_component_filter(
recipe_filter_flags = recipe_filter_flags::none ) const;

/** Prevent this recipe from ever being added to the player's learned recipies ( used for special NPC crafting ) */
bool never_learn = false;
Expand Down

0 comments on commit aa5b77c

Please sign in to comment.