diff --git a/data/help/texts.json b/data/help/texts.json index 645be937c55d7..5e679aacdc0bc 100644 --- a/data/help/texts.json +++ b/data/help/texts.json @@ -109,7 +109,8 @@ "To craft items, press . There are seven categories: Weapons, Ammo, Food, Chemicals, Electronics, Armor, and Other. In each major category there are several smaller sub-categories. While a few items require no particular skill to create, the majority require you to have some knowledge. Sometimes a skilled survivor will work out a given recipe from her or his knowledge of the skill, but more often you will need reference material, commonly a book of some sort. Reading such references gives a chance to memorize recipes outright, and you can also craft while referring to the book: just have it handy when crafting. Different knowledge is useful for different applications:", "-> Fabrication is the generic artisan skill, used for a wide variety of gear.\n-> Cooking, at low levels, is used for making tasty recipes; at higher levels, you have an understanding of chemistry and can make chemical weapons and beneficial elixirs.\n-> Tailoring is used to create basic clothing, and armor later on.\n-> Electronics lets you make a wide variety of tools with intricate uses.", "In addition to the primary crafting skills, other skills may be necessary to create certain items. Traps, Marksmanship, and First Aid are all required for certain items.", - "Crafting an item with high difficulty may fail and possibly waste some materials. You should prepare spare material, just in case." + "Crafting an item with high difficulty may fail and possibly waste some materials. You should prepare spare material, just in case.", + "Crafting very large/heavy items or batches of items is best done at a workbench of some kind. You could use any ordinary table, or build your own out of metal to handle even heavier loads." ] }, { diff --git a/data/json/construction.json b/data/json/construction.json index 11e8da88af5ae..5af88e557d077 100644 --- a/data/json/construction.json +++ b/data/json/construction.json @@ -1354,6 +1354,27 @@ "pre_special": "check_empty", "post_terrain": "f_table" }, + { + "type": "construction", + "description": "Build Workbench", + "category": "FURN", + "required_skills": [ [ "fabrication", 3 ] ], + "time": 60, + "using": [ [ "welding_standard", 5 ] ], + "components": [ [ [ "pipe", 8 ] ], [ [ "sheet_metal", 2 ] ], [ [ "sheet_metal_small", 4 ] ] ], + "pre_special": "check_empty", + "post_terrain": "f_workbench" + }, + { + "type": "construction", + "description": "Place Workbench", + "category": "FURN", + "required_skills": [ [ "fabrication", 0 ] ], + "time": 1, + "components": [ [ [ "workbench", 1 ] ] ], + "pre_special": "check_empty", + "post_terrain": "f_workbench" + }, { "type": "construction", "description": "Build Chair", diff --git a/data/json/furniture.json b/data/json/furniture.json index 3aa5ecb100a7e..f38144b7ab222 100644 --- a/data/json/furniture.json +++ b/data/json/furniture.json @@ -754,7 +754,9 @@ "sound": "smash!", "sound_fail": "whump.", "items": [ { "item": "2x4", "count": [ 1, 3 ] }, { "item": "nail", "charges": [ 2, 6 ] }, { "item": "splinter", "count": 1 } ] - } + }, + "examine_action": "workbench", + "workbench": { "multiplier": 1.0, "mass": 100000, "volume": "100L" } }, { "type": "furniture", @@ -864,7 +866,9 @@ "sound": "smash!", "sound_fail": "whump.", "items": [ { "item": "2x4", "count": [ 2, 6 ] }, { "item": "nail", "charges": [ 4, 8 ] }, { "item": "splinter", "count": 1 } ] - } + }, + "examine_action": "workbench", + "workbench": { "multiplier": 1.0, "mass": 100000, "volume": "100L" } }, { "type": "furniture", @@ -977,7 +981,9 @@ "sound": "smash!", "sound_fail": "whump.", "items": [ { "item": "2x4", "count": [ 2, 6 ] }, { "item": "nail", "charges": [ 4, 8 ] }, { "item": "splinter", "count": 1 } ] - } + }, + "examine_action": "workbench", + "workbench": { "multiplier": 1.0, "mass": 100000, "volume": "100L" } }, { "type": "furniture", @@ -4453,7 +4459,7 @@ "move_cost_mod": 2, "required_str": -1, "looks_like": "f_counter", - "flags": [ "TRANSPARENT", "PLACE_ITEM", "MOUNTABLE" ], + "flags": [ "TRANSPARENT", "PLACE_ITEM", "MOUNTABLE", "FLAT_SURF" ], "deconstruct": { "items": [ { "item": "pipe", "count": [ 6, 12 ] }, @@ -4479,7 +4485,9 @@ { "item": "cable", "charges": [ 1, 3 ] }, { "item": "cu_pipe", "count": 1 } ] - } + }, + "examine_action": "workbench", + "workbench": { "multiplier": 1.1, "mass": 150000, "volume": "150L" } }, { "type": "furniture", @@ -5509,5 +5517,62 @@ { "item": "scrap_copper", "count": [ 0, 2 ] } ] } + }, + { + "type": "furniture", + "id": "f_workbench", + "name": "workbench", + "description": "A sturdy workbench built out of metal. It is perfect for crafting large and heavy things.", + "symbol": "#", + "color": "red", + "move_cost_mod": 2, + "required_str": -1, + "looks_like": "f_lab_bench", + "flags": [ "TRANSPARENT", "PLACE_ITEM", "MOUNTABLE", "FLAT_SURF" ], + "deconstruct": { + "items": [ + { "item": "pipe", "count": [ 6, 8 ] }, + { "item": "sheet_metal", "count": 2 }, + { "item": "sheet_metal_small", "count": [ 2, 4 ] } + ] + }, + "max_volume": 4000, + "bash": { + "str_min": 35, + "str_max": 80, + "sound": "metal screeching!", + "sound_fail": "clang!", + "items": [ + { "item": "pipe", "count": [ 4, 6 ] }, + { "item": "sheet_metal", "count": [ 0, 1 ] }, + { "item": "sheet_metal_small", "count": [ 12, 24 ] }, + { "item": "steel_chunk", "count": [ 4, 8 ] }, + { "item": "scrap", "count": [ 12, 24 ] } + ] + }, + "examine_action": "workbench", + "workbench": { "multiplier": 1.2, "mass": 250000, "volume": "250L" } + }, + { + "type": "furniture", + "id": "f_fake_bench_hands", + "name": "fake workbench hands", + "description": "This fake workbench holds the stats for working on a wielded item.", + "symbol": "#", + "color": "red", + "move_cost_mod": 2, + "required_str": -1, + "workbench": { "multiplier": 1.0, "mass": 30000, "volume": "30L" } + }, + { + "type": "furniture", + "id": "f_fake_bench_ground", + "name": "fake workbench ground", + "description": "This fake workbench holds the stats for working on a item on the ground.", + "symbol": "#", + "color": "red", + "move_cost_mod": 2, + "required_str": -1, + "workbench": { "multiplier": 0.75, "mass": 50000, "volume": "100L" } } ] diff --git a/data/json/items/vehicle_parts.json b/data/json/items/vehicle_parts.json index d64694be3876b..e3d83725790f3 100644 --- a/data/json/items/vehicle_parts.json +++ b/data/json/items/vehicle_parts.json @@ -211,6 +211,21 @@ "category": "veh_parts", "price": 20000 }, + { + "type": "GENERIC", + "id": "workbench", + "name": "workbench", + "description": "A sturdy workbench built out of metal. It is perfect for crafting large and heavy things.", + "weight": 23000, + "to_hit": -8, + "color": "red", + "symbol": "0", + "material": [ "steel" ], + "volume": 42, + "bashing": 8, + "category": "veh_parts", + "price": 40000 + }, { "type": "GENERIC", "id": "saddle", diff --git a/data/json/recipes/recipe_others.json b/data/json/recipes/recipe_others.json index 6b85ef041c9ec..4d5939d1c24bf 100644 --- a/data/json/recipes/recipe_others.json +++ b/data/json/recipes/recipe_others.json @@ -837,6 +837,20 @@ "qualities": [ { "id": "HAMMER", "level": 1 } ], "components": [ [ [ "2x4", 6 ] ], [ [ "nail", 8 ] ] ] }, + { + "type": "recipe", + "result": "workbench", + "category": "CC_OTHER", + "subcategory": "CSC_OTHER_PARTS", + "skill_used": "fabrication", + "difficulty": 3, + "time": 60000, + "reversible": true, + "decomp_learn": 1, + "autolearn": true, + "using": [ [ "welding_standard", 5 ] ], + "components": [ [ [ "pipe", 8 ] ], [ [ "sheet_metal", 2 ] ], [ [ "sheet_metal_small", 4 ] ] ] + }, { "type": "recipe", "result": "foot_crank", diff --git a/data/json/vehicle_parts.json b/data/json/vehicle_parts.json index a4aebb7b730a3..950f9613f5ec6 100644 --- a/data/json/vehicle_parts.json +++ b/data/json/vehicle_parts.json @@ -969,12 +969,13 @@ "item": "v_table", "difficulty": 1, "location": "center", - "flags": [ "CARGO", "OBSTACLE", "TOOL_WRENCH", "FLAT_SURF" ], + "flags": [ "CARGO", "OBSTACLE", "TOOL_WRENCH", "FLAT_SURF", "WORKBENCH" ], "breaks_into": [ { "item": "2x4", "count": [ 1, 6 ] }, { "item": "splinter", "count": [ 4, 6 ] }, { "item": "nail", "charges": [ 4, 7 ] } - ] + ], + "workbench": { "multiplier": 1.0, "mass": 80000, "volume": "80L" } }, { "type": "vehicle_part", @@ -990,12 +991,38 @@ "item": "w_table", "difficulty": 1, "location": "center", - "flags": [ "CARGO", "OBSTACLE", "TOOL_WRENCH", "FLAT_SURF" ], + "flags": [ "CARGO", "OBSTACLE", "TOOL_WRENCH", "FLAT_SURF", "WORKBENCH" ], "breaks_into": [ { "item": "2x4", "count": [ 1, 6 ] }, { "item": "splinter", "count": [ 4, 6 ] }, { "item": "nail", "charges": [ 4, 7 ] } - ] + ], + "workbench": { "multiplier": 1.0, "mass": 80000, "volume": "80L" } + }, + { + "type": "vehicle_part", + "id": "workbench", + "name": "workbench", + "symbol": "#", + "color": "red", + "broken_symbol": "#", + "broken_color": "light_gray", + "looks_like": "veh_table_wood", + "damage_modifier": 60, + "durability": 300, + "size": 20, + "item": "workbench", + "difficulty": 1, + "location": "center", + "flags": [ "CARGO", "OBSTACLE", "TOOL_WRENCH", "FLAT_SURF", "WORKBENCH" ], + "breaks_into": [ + { "item": "pipe", "count": [ 4, 6 ] }, + { "item": "sheet_metal", "count": [ 0, 1 ] }, + { "item": "sheet_metal_small", "count": [ 12, 24 ] }, + { "item": "steel_chunk", "count": [ 4, 8 ] }, + { "item": "scrap", "count": [ 12, 24 ] } + ], + "workbench": { "multiplier": 1.2, "mass": 1500000, "volume": "150L" } }, { "type": "vehicle_part", diff --git a/data/json/vehicleparts/vp_flags.json b/data/json/vehicleparts/vp_flags.json index be8bec45bed1a..67eeb0590a978 100644 --- a/data/json/vehicleparts/vp_flags.json +++ b/data/json/vehicleparts/vp_flags.json @@ -148,5 +148,17 @@ "type": "json_flag", "context": [ "vehicle_part" ], "info": "If the center of balance of your vehicle is between all the wheels, the vehicle will be able to move, provided it has an active engine or motor with enough power to move the vehicle." + }, + { + "id": "FLAT_SURF", + "type": "json_flag", + "context": [ "vehicle_part" ], + "info": "This has a flat surface. It would be a nice place to eat." + }, + { + "id": "WORKBENCH", + "type": "json_flag", + "context": [ "vehicle_part" ], + "info": "You can craft here." } ] diff --git a/doc/JSON_FLAGS.md b/doc/JSON_FLAGS.md index e616bda33f3af..7e5206b7e4fda 100644 --- a/doc/JSON_FLAGS.md +++ b/doc/JSON_FLAGS.md @@ -80,7 +80,7 @@ List of known flags, used in both terrain.json and furniture.json - ```FLAMMABLE_ASH``` Burns to ash rather than rubble. - ```FLAMMABLE_HARD``` Harder to light on fire, but still possible. - ```FLAT``` Player can build and move furniture on. -- ```FLAT_SURF``` Furniture or terrain or vehicle part with flat hard surface (ex. table, but not chair; tree stump, etc.). +- ```FLAT_SURF``` Furniture or terrain with a flat hard surface (e.g. table, but not chair; tree stump, etc.). - ```GOES_DOWN``` Can use > to go down a level. - ```GOES_UP``` Can use < to go up a level. - ```HARVESTED``` Marks the harvested version of a terrain type (e.g. harvesting an apple tree turns it into a harvested tree, which later becomes an apple tree again). @@ -513,6 +513,7 @@ These branches are also the valid entries for the categories of `dreams` in `dre - ```EVENTURN``` Only on during even turns. - ```EXTRA_DRAG``` tells the vehicle that the part exerts engine power reduction. - ```FAUCET``` +- ```FLAT_SURF``` Part with a flat hard surface (e.g. table). - ```FOLDABLE``` - ```FORGE``` Acts as a forge for crafting. - ```FRIDGE``` Can refrigerate items. @@ -579,6 +580,7 @@ These branches are also the valid entries for the categories of `dreams` in `dre - ```WASHING_MACHINE``` Can be used to wash filthy clothes en masse. - ```WATER_WHEEL``` Recharges vehicle batteries when in flowing water. - ```WINDOW``` Can see through this part and can install curtains over it. +- ```WORKBENCH``` Can craft at this part, must be paired with a workbench json entry. ## Ammo diff --git a/doc/JSON_INFO.md b/doc/JSON_INFO.md index fc5e37105bb29..ca6762f165518 100644 --- a/doc/JSON_INFO.md +++ b/doc/JSON_INFO.md @@ -1632,8 +1632,8 @@ Optional message to be printed when a creature using the harvest definition is b #### `entries` -Array of dictionaries defining possible items produced on butchering and their likelihood of being produced. -`drop` value should be the `id` string of the item to be produced. +Array of dictionaries defining possible items produced on butchering and their likelihood of being produced. +`drop` value should be the `id` string of the item to be produced. Acceptable values are as follows: `flesh`: the "meat" of the creature. `offal`: the "organs" of the creature. these are removed when field dressing. @@ -1644,7 +1644,7 @@ Acceptable values are as follows: `type` value should be a string with the associated body part the item comes from. -`base_num` value should be an array with two elements in which the first defines the minimum number of the corresponding item produced and the second defines the maximum number. +`base_num` value should be an array with two elements in which the first defines the minimum number of the corresponding item produced and the second defines the maximum number. `scale_num` value should be an array with two elements, increasing the minimum and maximum drop numbers respectively by element value * survival skill. @@ -1662,14 +1662,16 @@ Acceptable values are as follows: "color": "white", "move_cost_mod": 2, "required_str": 18, - "flags": ["TRANSPARENT", "BASHABLE", "FLAMMABLE_HARD"], + "flags": [ "TRANSPARENT", "BASHABLE", "FLAMMABLE_HARD" ], "crafting_pseudo_item": "anvil", "examine_action": "toilet", "close": "f_foo_closed", "open": "f_foo_open", "bash": "TODO", "deconstruct": "TODO", - "max_volume": 4000 + "max_volume": 4000, + "examine_action": "workbench", + "workbench": { "multiplier": 1.1, "mass": 10000, "volume": "50L" } } ``` @@ -1693,6 +1695,10 @@ Strength required to move the furniture around. Negative values indicate an unmo (Optional) Id of an item (tool) that will be available for crafting when this furniture is range (the furniture acts as an item of that type). +#### `workbench` + +(Optional) Can craft here. Must specify a speed multiplier, allowed mass, and allowed volume. Mass/volume over these limits incur a speed penalty. Must be paired with a `"workbench"` `examine_action` to function. + ### Terrain ```C++ diff --git a/src/activity_handlers.cpp b/src/activity_handlers.cpp index 12445db940a60..4e04b54f95d71 100644 --- a/src/activity_handlers.cpp +++ b/src/activity_handlers.cpp @@ -2641,41 +2641,35 @@ void activity_handlers::craft_do_turn( player_activity *act, player *p ) { item *craft = act->targets.front().get_item(); - if( !craft->is_craft() ) { - debugmsg( "ACT_CRAFT target '%s' is not a craft. Aborting ACT_CRAFT.", craft->tname() ); + // item_location::get_item() will return nullptr if the item is lost + if( !craft ) { + p->add_msg_player_or_npc( + string_format( _( "You no longer have the %1$s in your possession. You stop crafting. " + " Reactivate the %1$s to continue crafting." ), craft->tname() ), + string_format( _( " no longer has the %s in their possession. stops" + " crafting." ), craft->tname() ) + ); p->cancel_activity(); return; } - if( !p->has_item( *craft ) ) { - p->add_msg_player_or_npc( - string_format( - _( "You no longer have the %1$s in your possession. You stop crafting. Reactivate the %1$s to continue crafting." ), - craft->tname() ), - string_format( - _( " no longer has the %s in their possession. stops crafting." ), - craft->tname() ) - ); + + if( !craft->is_craft() ) { + debugmsg( "ACT_CRAFT target '%s' is not a craft. Aborting ACT_CRAFT.", craft->tname() ); p->cancel_activity(); return; } const recipe &rec = craft->get_making(); - const float crafting_speed = p->crafting_speed_multiplier( rec, true ); + const tripoint loc = act->targets.front().where() == item_location::type::character ? + tripoint_zero : act->targets.front().position(); + const float crafting_speed = p->crafting_speed_multiplier( *craft, loc ); + const int assistants = p->available_assistant_count( craft->get_making() ); const bool is_long = act->values[0]; if( crafting_speed <= 0.0f ) { - if( p->lighting_craft_speed_multiplier( rec ) <= 0.0f ) { - p->add_msg_if_player( m_bad, _( "You can no longer see well enough to keep crafting." ) ); - } else { - p->add_msg_if_player( m_bad, _( "You are too frustrated to continue and just give up." ) ); - } p->cancel_activity(); return; } - if( calendar::once_every( 1_hours ) && crafting_speed < 0.75f ) { - // TODO: Describe the causes of slowdown - p->add_msg_if_player( m_bad, _( "You can't focus and are working slowly." ) ); - } // item_counter represents the percent progress relative to the base batch time // stored precise to 5 decimal places ( e.g. 67.32 percent would be stored as 6732000 ) @@ -2684,7 +2678,8 @@ void activity_handlers::craft_do_turn( player_activity *act, player *p ) // Must ensure >= 1 so we don't divide by 0; const double base_total_moves = std::max( 1, rec.batch_time( craft->charges, 1.0f, 0 ) ); // Current expected total moves, includes crafting speed modifiers and assistants - const double cur_total_moves = std::max( 1, p->expected_time_to_craft( rec, craft->charges ) ); + const double cur_total_moves = std::max( 1, rec.batch_time( craft->charges, crafting_speed, + assistants ) ); // Delta progress in moves adjusted for current crafting speed const double delta_progress = p->get_moves() * base_total_moves / cur_total_moves; // Current progress in moves @@ -2696,12 +2691,13 @@ void activity_handlers::craft_do_turn( player_activity *act, player *p ) // if item_counter has reached 100% or more if( craft->item_counter >= 10000000 ) { + item craft_copy = *craft; + act->targets.front().remove_item(); p->cancel_activity(); - item craft_copy = p->i_rem( craft ); - p->complete_craft( craft_copy ); + p->complete_craft( craft_copy, loc ); if( is_long ) { if( p->making_would_work( p->lastrecipe, craft_copy.charges ) ) { - p->last_craft->execute(); + p->last_craft->execute( loc ); } } } diff --git a/src/cata_utility.h b/src/cata_utility.h index 26e32f17566c7..0a2408c68c789 100644 --- a/src/cata_utility.h +++ b/src/cata_utility.h @@ -503,11 +503,12 @@ bool string_starts_with( const std::string &s1, const std::string &s2 ); */ bool string_ends_with( const std::string &s1, const std::string &s2 ); -/** Used as a default in function declarations in visitable.h, inventory.h, and player.h */ -const std::function return_true = []( const item & ) +/** Used as a default filter in various functions */ +template +bool return_true( const T & ) { return true; -}; +} /** * Joins a vector of `std::string`s into a single string with a delimiter/joiner diff --git a/src/craft_command.cpp b/src/craft_command.cpp index 0d501822931a0..fefb3978ecb35 100644 --- a/src/craft_command.cpp +++ b/src/craft_command.cpp @@ -32,12 +32,16 @@ std::string comp_selection::nname() const return item::nname( comp.type, comp.count ); } -void craft_command::execute() +void craft_command::execute( const tripoint &new_loc ) { if( empty() ) { return; } + if( new_loc != tripoint_zero ) { + loc = new_loc; + } + bool need_selections = true; inventory map_inv; map_inv.form_from_map( crafter->pos(), PICKUP_RANGE ); @@ -79,7 +83,7 @@ void craft_command::execute() } } - crafter->start_craft( *this ); + crafter->start_craft( *this, loc ); crafter->last_batch = batch_size; crafter->lastrecipe = rec->ident(); diff --git a/src/craft_command.h b/src/craft_command.h index c4e0ed3cfc814..668133c41b3f7 100644 --- a/src/craft_command.h +++ b/src/craft_command.h @@ -5,6 +5,7 @@ #include #include +#include "enums.h" #include "requirements.h" #include "string_id.h" @@ -54,11 +55,12 @@ class craft_command public: /** Instantiates an empty craft_command, which can't be executed. */ craft_command() = default; - craft_command( const recipe *to_make, int batch_size, bool is_long, player *crafter ) : - rec( to_make ), batch_size( batch_size ), longcraft( is_long ), crafter( crafter ) {} + craft_command( const recipe *to_make, int batch_size, bool is_long, player *crafter, + const tripoint loc = tripoint_zero ) : + rec( to_make ), batch_size( batch_size ), longcraft( is_long ), crafter( crafter ), loc( loc ) {} /** Selects components to use for the craft, then assigns the crafting activity to 'crafter'. */ - void execute(); + void execute( const tripoint &new_loc = tripoint_zero ); /** * Consumes the selected components and returns the resulting in progress craft item. @@ -89,6 +91,10 @@ class craft_command // This is mainly here for maintainability reasons. player *crafter; + // Location of the workbench to place the item on + // zero_tripoint indicates crafting without a workbench + tripoint loc = tripoint_zero; + std::vector> item_selections; std::vector> tool_selections; diff --git a/src/crafting.cpp b/src/crafting.cpp index e688a7dcefd8e..ab6205e16bec6 100644 --- a/src/crafting.cpp +++ b/src/crafting.cpp @@ -15,8 +15,10 @@ #include "game_inventory.h" #include "inventory.h" #include "item.h" +#include "item_location.h" #include "itype.h" #include "map.h" +#include "map_iterator.h" #include "messages.h" #include "npc.h" #include "options.h" @@ -27,8 +29,10 @@ #include "translations.h" #include "ui.h" #include "vehicle.h" +#include "vehicle_selector.h" #include "vpart_position.h" #include "vpart_reference.h" +#include "veh_type.h" const efftype_id effect_contacts( "contacts" ); @@ -108,53 +112,170 @@ float player::morale_crafting_speed_multiplier( const recipe &rec ) const return 1.0f / morale_effect; } +template +static float lerped_multiplier( const T &value, const T &low, const T &high ) +{ + // No effect if less than allowed value + if( value < low ) { + return 1.0f; + } + // Bottom out at 25% speed + if( value > high ) { + return 0.25f; + } + // Linear interpolation between high and low + // y = y0 + ( x - x0 ) * ( y1 - y0 ) / ( x1 - x0 ) + return 1.0f + ( value - low ) * ( 0.25f - 1.0f ) / ( high - low ); +} + +static float workbench_crafting_speed_multiplier( const item &craft, const tripoint &loc ) +{ + float multiplier; + units::mass allowed_mass; + units::volume allowed_volume; + + if( loc == tripoint_zero ) { + // tripoint_zero indicates crafting from inventory + // Use values from f_fake_bench_hands + const furn_t &f = string_id( "f_fake_bench_hands" ).obj(); + multiplier = f.workbench->multiplier; + allowed_mass = f.workbench->allowed_mass; + allowed_volume = f.workbench->allowed_volume; + } else if( const cata::optional vp = g->m.veh_at( + loc ).part_with_feature( "WORKBENCH", true ) ) { + // Vehicle workbench + const vpart_info &vp_info = vp->part().info(); + if( const cata::optional &wb_info = vp_info.get_workbench_info() ) { + multiplier = wb_info->multiplier; + allowed_mass = wb_info->allowed_mass; + allowed_volume = wb_info->allowed_volume; + } else { + debugmsg( "part '%S' with WORKBENCH flag has no workbench info", vp->part().name() ); + return 0.0f; + } + } else if( g->m.furn( loc ).obj().workbench ) { + // Furniture workbench + const furn_t &f = g->m.furn( loc ).obj(); + multiplier = f.workbench->multiplier; + allowed_mass = f.workbench->allowed_mass; + allowed_volume = f.workbench->allowed_volume; + } else { + // Ground + const furn_t &f = string_id( "f_fake_bench_ground" ).obj(); + multiplier = f.workbench->multiplier; + allowed_mass = f.workbench->allowed_mass; + allowed_volume = f.workbench->allowed_volume; + } + + const units::mass &craft_mass = craft.weight(); + const units::volume &craft_volume = craft.volume(); + + multiplier *= lerped_multiplier( craft_mass, allowed_mass, 1000_kilogram ); + multiplier *= lerped_multiplier( craft_volume, allowed_volume, 1000000_ml ); + + return multiplier; +} + float player::crafting_speed_multiplier( const recipe &rec, bool in_progress ) const { - float result = morale_crafting_speed_multiplier( rec ) * lighting_craft_speed_multiplier( rec ); + const float result = morale_crafting_speed_multiplier( rec ) * + lighting_craft_speed_multiplier( rec ); // Can't start if we'd need 300% time, but we can still finish the job if( !in_progress && result < 0.33f ) { return 0.0f; } - // If we're working below 20% speed, just give up - if( result < 0.2f ) { + // If we're working below 10% speed, just give up + if( result < 0.1f ) { return 0.0f; } return result; } +float player::crafting_speed_multiplier( const item &craft, const tripoint &loc ) const +{ + if( !craft.is_craft() ) { + debugmsg( "Can't calculate crafting speed multiplier of non-craft '%s'", craft.tname() ); + return 1.0f; + } + + const recipe &rec = craft.get_making(); + + const float light_multi = lighting_craft_speed_multiplier( rec ); + const float bench_multi = workbench_crafting_speed_multiplier( craft, loc ); + const float morale_multi = morale_crafting_speed_multiplier( rec ); + + const float total_multi = light_multi * bench_multi * morale_multi; + + if( light_multi <= 0.0f ) { + add_msg_if_player( m_bad, _( "You can no longer see well enough to keep crafting." ) ); + return 0.0f; + } + if( bench_multi <= 0.1f || ( bench_multi <= 0.33f && total_multi <= 0.2f ) ) { + add_msg_if_player( m_bad, _( "The %s is too large and/or heavy to work on. You may want to" + " use a workbench or a smaller batch size" ), craft.tname() ); + return 0.0f; + } + if( morale_multi <= 0.2f || ( morale_multi <= 0.33f && total_multi <= 0.2f ) ) { + add_msg_if_player( m_bad, _( "Your morale is too low to continue crafting." ) ); + return 0.0f; + } + + // If we're working below 20% speed, just give up + if( total_multi <= 0.2f ) { + add_msg_if_player( m_bad, _( "You are too frustrated to continue and just give up." ) ); + return 0.0f; + } + + if( calendar::once_every( 1_hours ) && total_multi < 0.75f ) { + if( light_multi <= 0.5f ) { + add_msg_if_player( m_bad, _( "You can't see well and are working slowly." ) ); + } + if( bench_multi <= 0.5f ) { + add_msg_if_player( m_bad, + _( "The %s is to large and/or heavy to work on comfortably. You are" + " working slowly." ), craft.tname() ); + } + if( morale_multi <= 0.5f ) { + add_msg_if_player( m_bad, _( "You can't focus and are working slowly." ) ); + } + } + + return total_multi; +} + bool player::has_morale_to_craft() const { return get_morale_level() >= -50; } -void player::craft() +void player::craft( const tripoint &loc ) { int batch_size = 0; const recipe *rec = select_crafting_recipe( batch_size ); if( rec ) { if( crafting_allowed( *this, *rec ) ) { - make_craft( rec->ident(), batch_size ); + make_craft( rec->ident(), batch_size, loc ); } } } -void player::recraft() +void player::recraft( const tripoint &loc ) { if( lastrecipe.str().empty() ) { popup( _( "Craft something first" ) ); } else if( making_would_work( lastrecipe, last_batch ) ) { - last_craft->execute(); + last_craft->execute( loc ); } } -void player::long_craft() +void player::long_craft( const tripoint &loc ) { int batch_size = 0; const recipe *rec = select_crafting_recipe( batch_size ); if( rec ) { if( crafting_allowed( *this, *rec ) ) { - make_all_craft( rec->ident(), batch_size ); + make_all_craft( rec->ident(), batch_size, loc ); } } } @@ -177,11 +298,11 @@ bool player::making_would_work( const recipe_id &id_to_make, int batch_size ) return check_eligible_containers_for_crafting( making, batch_size ); } -size_t available_assistant_count( const player &u, const recipe &rec ) +int player::available_assistant_count( const recipe &rec ) const { // NPCs around you should assist in batch production if they have the skills // TODO: Cache them in activity, include them in modifier calculations - const auto helpers = u.get_crafting_helpers(); + const auto helpers = get_crafting_helpers(); return std::count_if( helpers.begin(), helpers.end(), [&]( const npc * np ) { return np->get_skill_level( rec.skill_used ) >= rec.difficulty; @@ -190,14 +311,14 @@ size_t available_assistant_count( const player &u, const recipe &rec ) int player::base_time_to_craft( const recipe &rec, int batch_size ) const { - const size_t assistants = available_assistant_count( *this, rec ); + const size_t assistants = available_assistant_count( rec ); return rec.batch_time( batch_size, 1.0f, assistants ); } -int player::expected_time_to_craft( const recipe &rec, int batch_size ) const +int player::expected_time_to_craft( const recipe &rec, int batch_size, bool in_progress ) const { - const size_t assistants = available_assistant_count( *this, rec ); - float modifier = crafting_speed_multiplier( rec ); + const size_t assistants = available_assistant_count( rec ); + float modifier = crafting_speed_multiplier( rec, in_progress ); return rec.batch_time( batch_size, modifier, assistants ); } @@ -360,17 +481,18 @@ void player::invalidate_crafting_inventory() cached_time = calendar::before_time_starts; } -void player::make_craft( const recipe_id &id_to_make, int batch_size ) +void player::make_craft( const recipe_id &id_to_make, int batch_size, const tripoint &loc ) { - make_craft_with_command( id_to_make, batch_size ); + make_craft_with_command( id_to_make, batch_size, false, loc ); } -void player::make_all_craft( const recipe_id &id_to_make, int batch_size ) +void player::make_all_craft( const recipe_id &id_to_make, int batch_size, const tripoint &loc ) { - make_craft_with_command( id_to_make, batch_size, true ); + make_craft_with_command( id_to_make, batch_size, true, loc ); } -void player::make_craft_with_command( const recipe_id &id_to_make, int batch_size, bool is_long ) +void player::make_craft_with_command( const recipe_id &id_to_make, int batch_size, bool is_long, + const tripoint &loc ) { const auto &recipe_to_make = *id_to_make; @@ -378,7 +500,7 @@ void player::make_craft_with_command( const recipe_id &id_to_make, int batch_siz return; } - *last_craft = craft_command( &recipe_to_make, batch_size, is_long, this ); + *last_craft = craft_command( &recipe_to_make, batch_size, is_long, this, loc ); last_craft->execute(); } @@ -422,9 +544,23 @@ static void finalize_crafted_item( item &newit ) } } +static cata::optional wield_craft( player &p, item &craft ) +{ + if( p.wield( craft ) ) { + if( p.weapon.invlet ) { + p.add_msg_if_player( m_info, _( "Wielding %c - %s" ), p.weapon.invlet, + p.weapon.display_name().c_str() ); + } else { + p.add_msg_if_player( m_info, _( "Wielding - %s" ), p.weapon.display_name().c_str() ); + } + return item_location( p, &p.weapon ); + } + return cata::nullopt; +} + static item *set_item_inventory( player &p, item &newit ) { - item *ret_val = &null_item_reference(); + item *ret_val = nullptr; if( newit.made_of( LIQUID ) ) { g->handle_all_liquid( newit, PICKUP_RANGE ); } else { @@ -443,6 +579,60 @@ static item *set_item_inventory( player &p, item &newit ) return ret_val; } +/** + * Set an item on the map or in a vehicle and return the new location + * + */ +static item_location set_item_map_or_vehicle( const player &p, const tripoint &loc, item &newit ) +{ + if( const cata::optional vp = g->m.veh_at( loc ).part_with_feature( "CARGO", + false ) ) { + + if( vp->vehicle().add_item( vp->part_index(), newit ) ) { + + // Since add succeded, we know that the craft is the last item in the vehicle_stack + auto items_at_part = vp->vehicle().get_items( vp->part_index() ); + item *newit_in_vehicle = &items_at_part[items_at_part.size() - 1]; + + p.add_msg_player_or_npc( + string_format( pgettext( "item, furniture", "You put the %s on the %s." ), + newit.tname(), vp->part().name() ), + string_format( pgettext( "item, furniture", " puts the %s on the %s." ), + newit.tname(), vp->part().name() ) ); + + return item_location( vehicle_cursor( vp->vehicle(), vp->part_index() ), newit_in_vehicle ); + } + + // Couldn't add the in progress craft to the target part, so drop it to the map. + p.add_msg_player_or_npc( + string_format( pgettext( "furniture, item", + "Not enough space on the %s. You drop the %s on the ground." ), + vp->part().name(), newit.tname() ), + string_format( pgettext( "furniture, item", + "Not enough space on the %s. drops the %s on the ground." ), + vp->part().name(), newit.tname() ) ); + + return item_location( map_cursor( loc ), &g->m.add_item_or_charges( loc, newit ) ); + + } else { + if( g->m.has_furn( loc ) ) { + const furn_t &workbench = g->m.furn( loc ).obj(); + p.add_msg_player_or_npc( + string_format( pgettext( "item, furniture", "You put the %s on the %s." ), + newit.tname(), workbench.name() ), + string_format( pgettext( "item, furniture", " puts the %s on the %s." ), + newit.tname(), workbench.name() ) ); + } else { + p.add_msg_player_or_npc( + string_format( pgettext( "item", "You put the %s on the ground." ), + newit.tname() ), + string_format( pgettext( "item", " puts the %s on the ground." ), + newit.tname() ) ); + } + return item_location( map_cursor( loc ), &g->m.add_item_or_charges( loc, newit ) ); + } +} + static void return_all_components_for_craft( player &p, std::list &used, const double &relative_rot ) { @@ -471,7 +661,7 @@ static void return_some_components_for_craft( player &p, std::list &used, } } -void player::start_craft( craft_command &command ) +void player::start_craft( craft_command &command, const tripoint &loc ) { if( command.empty() ) { debugmsg( "Attempted to start craft with empty command" ); @@ -485,22 +675,114 @@ void player::start_craft( craft_command &command ) reset_encumbrance(); } - item *craft_in_inventory = set_item_inventory( *this, craft ); - if( !has_item( *craft_in_inventory ) ) { - add_msg_if_player( _( "Activate the %s to start crafting" ), craft.tname() ); + item_location craft_in_world; + + // Crafting without a workbench + if( loc == tripoint_zero ) { + if( !is_armed() ) { + if( cata::optional it_loc = wield_craft( *this, craft ) ) { + craft_in_world = it_loc->clone(); + } else { + // This almost certianly shouldn't happen + put_into_vehicle_or_drop( *this, item_drop_reason::tumbling, {craft} ); + } + } else { + enum option : int { + WIELD_CRAFT = 0, + BENCH_CRAFT, + DROP_CRAFT, + STASH, + DROP + }; + + uilist amenu; + amenu.text = string_format( pgettext( "in progress craft", "What to do with the %s?" ), + craft.display_name() ); + amenu.addentry( WIELD_CRAFT, !weapon.has_flag( "NO_UNWIELD" ), '1', + string_format( _( "Dispose of your wielded %s and start working." ), weapon.tname() ) ); + const bool adjacent_workbench = g->m.has_adjacent_furniture_with( pos(), []( const furn_t &f ) { + std::cout << f.name() << std::endl; + return !!f.workbench; + } ); + amenu.addentry( BENCH_CRAFT, adjacent_workbench, '2', + _( "Put it on a workbench and start working." ) ); + amenu.addentry( DROP_CRAFT, true, '3', _( "Put it down and start working." ) ); + const bool can_stash = can_pickVolume( craft ) && + can_pickWeight( craft, !get_option( "DANGEROUS_PICKUPS" ) ); + amenu.addentry( STASH, can_stash, '4', _( "Store it in your inventory." ) ); + amenu.addentry( DROP, true, '5', _( "Drop it on the ground." ) ); + + amenu.query(); + const option choice = amenu.ret == UILIST_CANCEL ? DROP : static_cast