From 142fdfe0ecfb8368663da0d83c3fbbabdb61a2d2 Mon Sep 17 00:00:00 2001 From: John Bytheway Date: Tue, 31 Dec 2019 22:23:11 -0500 Subject: [PATCH] Detect and warn about crafting with rotten food 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. If the result doesn't care about rottenness, as with e.g. lamp oil, then no warnings or marking of the recipes is done. 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. --- src/craft_command.cpp | 22 ++++++++--- src/craft_command.h | 3 ++ src/crafting.cpp | 19 +++++++-- src/crafting_gui.cpp | 92 ++++++++++++++++++++++--------------------- src/player.h | 3 +- src/recipe.cpp | 9 ++++- src/recipe.h | 14 ++++++- 7 files changed, 105 insertions(+), 57 deletions(-) diff --git a/src/craft_command.cpp b/src/craft_command.cpp index 73018b9fdae28..0800bf519e065 100644 --- a/src/craft_command.cpp +++ b/src/craft_command.cpp @@ -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; @@ -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 is = crafter->select_item_component( it, batch_size, map_inv, true, - filter ); + comp_selection is = + crafter->select_item_component( it, batch_size, map_inv, true, filter ); if( is.use_from == cancel ) { return; } @@ -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 tmp = crafter->consume_items( it, batch_size, filter ); @@ -273,7 +283,7 @@ std::vector> craft_command::check_item_components_miss { std::vector> 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; diff --git a/src/craft_command.h b/src/craft_command.h index a0071e8e41634..ac38b16a9350e 100644 --- a/src/craft_command.h +++ b/src/craft_command.h @@ -6,6 +6,7 @@ #include #include "point.h" +#include "recipe.h" #include "requirements.h" class inventory; @@ -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; diff --git a/src/crafting.cpp b/src/crafting.cpp index 65e81f1b2aff0..e91141033871d 100644 --- a/src/crafting.cpp +++ b/src/crafting.cpp @@ -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; @@ -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 ) @@ -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 filter = rec.get_component_filter(); + std::function filter = rec.get_component_filter(); + const std::function 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; @@ -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 ); diff --git a/src/crafting_gui.cpp b/src/crafting_gui.cpp index 41c7f84b3ee10..d3b055b0704b9 100644 --- a/src/crafting_gui.cpp +++ b/src/crafting_gui.cpp @@ -192,7 +192,26 @@ const recipe *select_crafting_recipe( int &batch_size ) list_circularizer tab( craft_cat_list ); list_circularizer subtab( craft_subcat_list[tab.cur()] ); std::vector current; - std::vector 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 available; const int componentPrintHeight = dataHeight - tailHeight - 1; //preserves component color printout between mode rotations nc_color rotated_color = c_white; @@ -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 availability_cache; + std::map availability_cache; do { if( redraw ) { @@ -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 picking; @@ -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 ); } ); } @@ -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 ) { @@ -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 ) { @@ -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( dataHeight ) + 1; ++i ) { + for( int i = 0; i < static_cast( current.size() ) && i < dataHeight + 1; ++i ) { std::string tmp_name = current[i]->result_name(); if( batch ) { - tmp_name = string_format( _( "%2dx %s" ), static_cast( i ) + 1, tmp_name ); - } - if( static_cast( 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(); @@ -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, + _( "Will use rotten ingredients" ) ); + } ypos += print_items( *current[line], w_data, ypos, xpos, col, batch ? line + 1 : 1 ); } @@ -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 ) ) { diff --git a/src/player.h b/src/player.h index b6f4a5d873257..469ebf77180a1 100644 --- a/src/player.h +++ b/src/player.h @@ -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; @@ -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 ); /** diff --git a/src/recipe.cpp b/src/recipe.cpp index c17b6d77a9260..e0ac262b4ddc3 100644 --- a/src/recipe.cpp +++ b/src/recipe.cpp @@ -500,14 +500,19 @@ bool recipe::will_be_blacklisted() const return any_is_blacklisted( reqs_internal ) || any_is_blacklisted( reqs_external ); } -std::function recipe::get_component_filter() const +std::function 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( flags & recipe_filter_flags::no_rotten ) && result.goes_bad(); std::function rotten_filter = return_true; - 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(); }; diff --git a/src/recipe.h b/src/recipe.h index c5170b370e963..89374a519f572 100644 --- a/src/recipe.h +++ b/src/recipe.h @@ -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( + static_cast( l ) & static_cast( r ) ); +} + class recipe { friend class recipe_dictionary; @@ -66,7 +77,8 @@ class recipe // recipe finalization happens bool will_be_blacklisted() const; - std::function get_component_filter() const; + std::function 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;