diff --git a/data/json/item_actions.json b/data/json/item_actions.json index 747044ab3ce73..34afe2b65e8bd 100644 --- a/data/json/item_actions.json +++ b/data/json/item_actions.json @@ -1028,5 +1028,10 @@ "type": "item_action", "id": "PANACEA", "name": "Take" + }, + { + "type": "item_action", + "id": "CRAFT", + "name": "Work on craft" } ] diff --git a/data/json/items/generic.json b/data/json/items/generic.json index fa15e52149aa0..51175fb6690f6 100644 --- a/data/json/items/generic.json +++ b/data/json/items/generic.json @@ -3007,5 +3007,19 @@ "weight": 5, "volume": 1, "bashing": 1 + }, + { + "type": "GENERIC", + "id": "craft", + "symbol": "%", + "color": "white", + "name": "in progress craft", + "description": "This is an in progress craft.", + "price": 0, + "weight": 1, + "volume": 1, + "max_charges": 20, + "use_action": "CRAFT", + "flags": [ "NO_SALVAGE", "TRADER_AVOID" ] } ] diff --git a/data/json/player_activities.json b/data/json/player_activities.json index 8cddb794c0357..96dc4f27e46dd 100644 --- a/data/json/player_activities.json +++ b/data/json/player_activities.json @@ -36,13 +36,6 @@ "based_on": "neither", "refuel_fires": true }, - { - "id": "ACT_LONGCRAFT", - "type": "activity_type", - "stop_phrase": "Stop crafting?", - "based_on": "neither", - "refuel_fires": true - }, { "id": "ACT_DISASSEMBLE", "type": "activity_type", diff --git a/src/activity_handlers.cpp b/src/activity_handlers.cpp index bc03e91c68d90..cfd97d815d035 100644 --- a/src/activity_handlers.cpp +++ b/src/activity_handlers.cpp @@ -27,6 +27,7 @@ #include "mtype.h" #include "output.h" #include "player.h" +#include "recipe.h" #include "requirements.h" #include "rng.h" #include "skill.h" @@ -57,7 +58,6 @@ const std::map< activity_id, std::function activity_handlers::do_turn_functions = { { activity_id( "ACT_BURROW" ), burrow_do_turn }, { activity_id( "ACT_CRAFT" ), craft_do_turn }, - { activity_id( "ACT_LONGCRAFT" ), craft_do_turn }, { activity_id( "ACT_FILL_LIQUID" ), fill_liquid_do_turn }, { activity_id( "ACT_PICKAXE" ), pickaxe_do_turn }, { activity_id( "ACT_DROP" ), drop_do_turn }, @@ -136,8 +136,6 @@ activity_handlers::finish_functions = { { activity_id( "ACT_WAIT_NPC" ), wait_npc_finish }, { activity_id( "ACT_SOCIALIZE" ), socialize_finish }, { activity_id( "ACT_TRY_SLEEP" ), try_sleep_finish }, - { activity_id( "ACT_CRAFT" ), craft_finish }, - { activity_id( "ACT_LONGCRAFT" ), longcraft_finish }, { activity_id( "ACT_DISASSEMBLE" ), disassemble_finish }, { activity_id( "ACT_BUILD" ), build_finish }, { activity_id( "ACT_VIBE" ), vibe_finish }, @@ -2632,8 +2630,30 @@ void activity_handlers::try_sleep_finish( player_activity *act, player *p ) void activity_handlers::craft_do_turn( player_activity *act, player *p ) { - const recipe &rec = recipe_id( act->name ).obj(); + 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() ); + 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() ) + ); + p->cancel_activity(); + return; + } + + const recipe &rec = craft->get_making(); const float crafting_speed = p->crafting_speed_multiplier( rec, true ); + 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." ) ); @@ -2643,28 +2663,23 @@ void activity_handlers::craft_do_turn( player_activity *act, player *p ) p->cancel_activity(); return; } - act->moves_left -= crafting_speed * p->get_moves(); - p->set_moves( 0 ); 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." ) ); } -} -void activity_handlers::craft_finish( player_activity *act, player *p ) -{ - p->complete_craft(); - act->set_to_null(); -} + craft->item_counter += crafting_speed * p->get_moves(); + p->set_moves( 0 ); -void activity_handlers::longcraft_finish( player_activity *act, player *p ) -{ - const int batch_size = act->values.front(); - p->complete_craft(); - act->set_to_null(); - // Workaround for a bug where longcraft can be unset in complete_craft(). - if( p->making_would_work( p->lastrecipe, batch_size ) ) { - p->last_craft->execute(); + if( craft->item_counter >= rec.time ) { + p->cancel_activity(); + item craft_copy = p->i_rem( craft ); + p->complete_craft( craft_copy ); + if( is_long ) { + if( p->making_would_work( p->lastrecipe, craft_copy.charges ) ) { + p->last_craft->execute(); + } + } } } diff --git a/src/activity_handlers.h b/src/activity_handlers.h index 7607db620d0d1..c14cf4c9139dd 100644 --- a/src/activity_handlers.h +++ b/src/activity_handlers.h @@ -120,8 +120,6 @@ void wait_weather_finish( player_activity *act, player *p ); void wait_npc_finish( player_activity *act, player *p ); void socialize_finish( player_activity *act, player *p ); void try_sleep_finish( player_activity *act, player *p ); -void craft_finish( player_activity *act, player *p ); -void longcraft_finish( player_activity *act, player *p ); void disassemble_finish( player_activity *act, player *p ); void build_finish( player_activity *act, player *p ); void vibe_finish( player_activity *act, player *p ); diff --git a/src/clzones.cpp b/src/clzones.cpp index 5ef78f01cef27..f2e635df5fa7c 100644 --- a/src/clzones.cpp +++ b/src/clzones.cpp @@ -517,7 +517,7 @@ zone_type_id zone_manager::get_near_zone_type_for_item( const item &it, const auto &it_food = it.is_food_container() ? it.contents.front() : it; if( it_food.is_food() ) { // skip food without comestible, like MREs - if( it_food.type->comestible->comesttype == "DRINK" ) { + if( it_food.get_comestible()->comesttype == "DRINK" ) { if( !preserves && it_food.goes_bad() && has_near( zone_type_id( "LOOT_PDRINK" ), where ) ) { return zone_type_id( "LOOT_PDRINK" ); } else if( has_near( zone_type_id( "LOOT_DRINK" ), where ) ) { diff --git a/src/consumption.cpp b/src/consumption.cpp index f5ed012b1a474..97861afdd64d2 100644 --- a/src/consumption.cpp +++ b/src/consumption.cpp @@ -115,7 +115,7 @@ int player::kcal_for( const item &comest ) const } kcal /= comest.recipe_charges; } else { - kcal = comest.type->comestible->get_calories(); + kcal = comest.get_comestible()->get_calories(); } if( has_trait( trait_GIZZARD ) ) { @@ -165,7 +165,7 @@ std::pair player::fun_for( const item &comest ) const } // As float to avoid rounding too many times - float fun = comest.type->comestible->fun; + float fun = comest.get_comestible()->fun; if( comest.has_flag( flag_MUSHY ) && fun > -5.0f ) { fun = -5.0f; // defrosted MUSHY food is practicaly tastless or tastes off } @@ -218,8 +218,8 @@ std::pair player::fun_for( const item &comest ) const } if( has_active_bionic( bio_taste_blocker ) && - power_level > abs( comest.type->comestible->fun ) && - comest.type->comestible->fun < 0 ) { + power_level > abs( comest.get_comestible()->fun ) && + comest.get_comestible()->fun < 0 ) { fun = 0; } @@ -259,7 +259,7 @@ std::map player::vitamins_from( const item &it ) const { std::map res; - if( !it.type->comestible ) { + if( !it.get_comestible() ) { return res; } @@ -277,7 +277,7 @@ std::map player::vitamins_from( const item &it ) const } } else { // if we're here, whatever is returned is going to be based on the item's defined stats - res = it.type->comestible->vitamins; + res = it.get_comestible()->vitamins; std::list traits = mut_vitamin_absorb_modify( *this ); // traits modify the absorption of vitamins here if( !traits.empty() ) { @@ -420,11 +420,15 @@ morale_type player::allergy_type( const item &food ) const ret_val player::can_eat( const item &food ) const { - const auto &comest = food.type->comestible; + const auto &comest = food.get_comestible(); if( !comest ) { return ret_val::make_failure( _( "That doesn't look edible." ) ); } + if( food.is_craft() ) { + return ret_val::make_failure( _( "That doesn't look edible in its current form." ) ); + } + if( food.item_tags.count( "DIRTY" ) ) { return ret_val::make_failure( _( "This is full of dirt after being on the ground." ) ); @@ -511,7 +515,7 @@ ret_val player::will_eat( const item &food, bool interactive ) co }; const bool saprophage = has_trait( trait_id( "SAPROPHAGE" ) ); - const auto &comest = food.type->comestible; + const auto &comest = food.get_comestible(); if( food.rotten() ) { const bool saprovore = has_trait( trait_id( "SAPROVORE" ) ); @@ -612,13 +616,13 @@ bool player::eat( item &food, bool force ) const bool hibernate = has_active_mutation( trait_id( "HIBERNATE" ) ); const int nutr = nutrition_for( food ); - const int quench = food.type->comestible->quench; + const int quench = food.get_comestible()->quench; const bool spoiled = food.rotten(); // The item is solid food - const bool chew = food.type->comestible->comesttype == "FOOD" || food.has_flag( "USE_EAT_VERB" ); + const bool chew = food.get_comestible()->comesttype == "FOOD" || food.has_flag( "USE_EAT_VERB" ); // This item is a drink and not a solid food (and not a thick soup) - const bool drinkable = !chew && food.type->comestible->comesttype == "DRINK"; + const bool drinkable = !chew && food.get_comestible()->comesttype == "DRINK"; // If neither of the above is true then it's a drug and shouldn't get mealtime penalty/bonus if( hibernate && @@ -750,9 +754,9 @@ bool player::eat( item &food, bool force ) } } - if( item::find_type( food.type->comestible->tool )->tool ) { + if( item::find_type( food.get_comestible()->tool )->tool ) { // Tools like lighters get used - use_charges( food.type->comestible->tool, 1 ); + use_charges( food.get_comestible()->tool, 1 ); } if( has_bionic( bio_ethanol ) && food.type->can_use( "ALCOHOL" ) ) { @@ -766,7 +770,7 @@ bool player::eat( item &food, bool force ) } if( has_active_bionic( bio_taste_blocker ) ) { - charge_power( -abs( food.type->comestible->fun ) ); + charge_power( -abs( food.get_comestible()->fun ) ); } if( food.has_flag( "CANNIBALISM" ) ) { @@ -874,8 +878,8 @@ bool player::eat( item &food, bool force ) // chance to become parasitised if( !( has_bionic( bio_digestion ) || has_trait( trait_id( "PARAIMMUNE" ) ) ) ) { - if( food.type->comestible->parasites > 0 && !food.has_flag( "NO_PARASITES" ) && - one_in( food.type->comestible->parasites ) ) { + if( food.get_comestible()->parasites > 0 && !food.has_flag( "NO_PARASITES" ) && + one_in( food.get_comestible()->parasites ) ) { switch( rng( 0, 3 ) ) { case 0: if( !has_trait( trait_id( "EATHEALTH" ) ) ) { @@ -940,7 +944,7 @@ void player::consume_effects( const item &food ) debugmsg( "called player::consume_effects with non-comestible" ); return; } - const auto &comest = *food.type->comestible; + const auto &comest = *food.get_comestible(); const int capacity = stomach_capacity(); if( has_trait( trait_id( "THRESH_PLANT" ) ) && food.type->can_use( "PLANTBLECH" ) ) { diff --git a/src/craft_command.cpp b/src/craft_command.cpp index efad4e676147c..42c3e3237b40f 100644 --- a/src/craft_command.cpp +++ b/src/craft_command.cpp @@ -76,15 +76,7 @@ void craft_command::execute() } } - auto type = activity_id( is_long ? "ACT_LONGCRAFT" : "ACT_CRAFT" ); - auto activity = player_activity( type, crafter->base_time_to_craft( *rec, batch_size ), -1, INT_MIN, - rec->ident().str() ); - activity.values.push_back( batch_size ); - activity.values.push_back( calendar::turn ); - activity.coords.push_back( crafter->pos() ); - - crafter->assign_activity( activity ); - + crafter->start_craft( *rec, batch_size, is_long ); crafter->last_batch = batch_size; crafter->lastrecipe = rec->ident(); diff --git a/src/craft_command.h b/src/craft_command.h index 4458f077d7664..0ffd7887340ba 100644 --- a/src/craft_command.h +++ b/src/craft_command.h @@ -72,7 +72,10 @@ class craft_command private: const recipe *rec = nullptr; int batch_size = 0; - /** Indicates the activity_type for this crafting job, Either ACT_CRAFT or ACT_LONGCRAFT. */ + /** + * Indicates whether the player has initiated a one off craft or wishes to craft as + * long as possible. + */ bool is_long = false; // This is mainly here for maintainability reasons. player *crafter; diff --git a/src/crafting.cpp b/src/crafting.cpp index ca4bd0b22affe..00266e671b488 100644 --- a/src/crafting.cpp +++ b/src/crafting.cpp @@ -383,7 +383,7 @@ void player::make_craft_with_command( const recipe_id &id_to_make, int batch_siz // @param offset is the index of the created item in the range [0, batch_size-1], // it makes sure that the used items are distributed equally among the new items. -void set_components( std::vector &components, const std::list &used, +void set_components( std::list &components, const std::list &used, const int batch_size, const size_t offset ) { if( batch_size <= 1 ) { @@ -432,29 +432,6 @@ std::list player::consume_components_for_craft( const recipe &making, int return used; } -std::list player::consume_some_components_for_craft( const recipe &making, int batch_size ) -{ - std::list used; - if( has_trait( trait_id( "DEBUG_HS" ) ) ) { - return used; - } - const auto &req = making.requirements(); - int cou = 0; - for( const auto &it : req.get_components() ) { - // Each component currently has 50% chance of not being consumed - // Skip first item so failed craft with one item recipe always loses component - if( cou == 0 || one_in( 2 ) ) { - std::list tmp = consume_items( it, batch_size ); - used.splice( used.end(), tmp ); - } - ++cou; - } - for( const auto &it : req.get_tools() ) { - consume_tools( it, batch_size ); - } - return used; -} - static void set_item_food( item &newit ) { // TODO: encapsulate this into some function @@ -469,37 +446,95 @@ static void finalize_crafted_item( item &newit ) } } -static void set_item_inventory( item &newit ) +static item *set_item_inventory( player &p, item &newit ) { + item *ret_val = &null_item_reference(); if( newit.made_of( LIQUID ) ) { g->handle_all_liquid( newit, PICKUP_RANGE ); } else { - g->u.inv.assign_empty_invlet( newit, g->u ); + p.inv.assign_empty_invlet( newit, p ); // We might not have space for the item - if( !g->u.can_pickVolume( newit ) ) { //Accounts for result_mult - put_into_vehicle_or_drop( g->u, item_drop_reason::too_large, { newit } ); - } else if( !g->u.can_pickWeight( newit, !get_option( "DANGEROUS_PICKUPS" ) ) ) { - put_into_vehicle_or_drop( g->u, item_drop_reason::too_heavy, { newit } ); + if( !p.can_pickVolume( newit ) ) { //Accounts for result_mult + put_into_vehicle_or_drop( p, item_drop_reason::too_large, { newit } ); + } else if( !p.can_pickWeight( newit, !get_option( "DANGEROUS_PICKUPS" ) ) ) { + put_into_vehicle_or_drop( p, item_drop_reason::too_heavy, { newit } ); } else { - newit = g->u.i_add( newit ); - add_msg( m_info, "%c - %s", newit.invlet == 0 ? ' ' : newit.invlet, newit.tname().c_str() ); + ret_val = &p.i_add( newit ); + add_msg( m_info, "%c - %s", ret_val->invlet == 0 ? ' ' : ret_val->invlet, + ret_val->tname().c_str() ); + } + } + return ret_val; +} + +static void return_all_components_for_craft( player &p, std::list &used, + const double &relative_rot ) +{ + for( item &it : used ) { + // If the product doesn't rot, don't touch component rot. + if( relative_rot != 0.0 ) { + it.set_relative_rot( relative_rot ); } + set_item_inventory( p, it ); } } -time_duration get_rot_since( const time_point &start, const time_point &end, - const tripoint &location ); // weather.cpp +static void return_some_components_for_craft( player &p, std::list &used, + const double &relative_rot ) +{ + for( std::list::iterator it = used.begin(); it != used.end(); ++it ) { + // Each component has a 50% chance of being returned + // Never return the first component + if( it != used.begin() && one_in( 2 ) ) { + // If the product doesn't rot, don't touch component rot. + if( relative_rot != 0.0 ) { + it->set_relative_rot( relative_rot ); + } + set_item_inventory( p, *it ); + } + } +} -void player::complete_craft() +void player::start_craft( const recipe &making, int batch_size, bool is_long ) { - const recipe &making = recipe_id( activity.name ).obj(); // Which recipe is it? - int batch_size = activity.values.front(); if( making.ident().is_null() ) { debugmsg( "no recipe with id %s found", activity.name ); - activity.set_to_null(); return; } + // Use up the components and tools + const std::list used = consume_components_for_craft( making, batch_size ); + if( last_craft->has_cached_selections() && used.empty() ) { + // This signals failure, even though there seem to be several paths where it shouldn't... + return; + } + if( !used.empty() ) { + reset_encumbrance(); // in case we were wearing something just consumed + } + + item craft( &making, batch_size, used ); + + 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() ); + } else { + add_msg_player_or_npc( + string_format( pgettext( "in progress craft", "You start working on the %s" ), + craft.tname() ), + string_format( pgettext( "in progress craft", " starts working on the %s" ), + craft.tname() ) ); + assign_activity( activity_id( "ACT_CRAFT" ) ); + activity.targets.push_back( item_location( *this, craft_in_inventory ) ); + activity.values.push_back( is_long ); + } +} + +void player::complete_craft( item &craft ) +{ + const recipe &making = craft.get_making(); // Which recipe is it? + const int batch_size = craft.charges; + + /* to be modified and moved to ACT_CRAFT */ int secondary_dice = 0; int secondary_difficulty = 0; for( const auto &pr : making.required_skills ) { @@ -515,7 +550,8 @@ void player::complete_craft() skill_dice = get_skill_level( making.skill_used ) * 4; } - auto helpers = g->u.get_crafting_helpers(); + auto helpers = get_crafting_helpers(); + for( const npc *np : helpers ) { if( np->get_skill_level( making.skill_used ) >= get_skill_level( making.skill_used ) ) { @@ -601,59 +637,22 @@ void player::complete_craft() } } + std::list &used = craft.components; + const double relative_rot = craft.get_relative_rot(); + // Messed up badly; waste some components. if( making.difficulty != 0 && diff_roll > skill_roll * ( 1 + 0.1 * rng( 1, 5 ) ) ) { add_msg( m_bad, _( "You fail to make the %s, and waste some materials." ), making.result_name() ); - consume_some_components_for_craft( making, batch_size ); - activity.set_to_null(); + return_some_components_for_craft( *this, used, relative_rot ); return; // Messed up slightly; no components wasted. } else if( diff_roll > skill_roll ) { add_msg( m_neutral, _( "You fail to make the %s, but don't waste any materials." ), making.result_name() ); - //this method would only have been called from a place that nulls activity.type, - //so it appears that it's safe to NOT null that variable here. - //rationale: this allows certain contexts (e.g. ACT_LONGCRAFT) to distinguish major and minor failures + return_all_components_for_craft( *this, used, relative_rot ); return; } - // If we're here, the craft was a success! - // Use up the components and tools - std::list used = consume_components_for_craft( making, batch_size ); - if( last_craft->has_cached_selections() && used.empty() ) { - // This signals failure, even though there seem to be several paths where it shouldn't... - return; - } - if( !used.empty() ) { - reset_encumbrance(); // in case we were wearing something just consumed up. - } - - const time_point now = calendar::turn; - time_point start_turn = now; - tripoint craft_pos = pos(); - if( activity.values.size() > 1 && !activity.coords.empty() ) { - start_turn = activity.values.at( 1 ); - craft_pos = activity.coords.at( 0 ); - } else { - // either something went wrong or player had an old binary and saved - // the game right in the middle of crafting, and then updated their - // binary, so we didn't grab these values before starting the craft - debugmsg( "Missing activity start time and temperature, using current val" ); - } - const time_duration rot_points = get_rot_since( start_turn, now, craft_pos ); - double max_relative_rot = 0; - // We need to cycle all the used ingredients and find the most rotten item, - // this will then set our relative rot for the crafted items. - for( const item &it : used ) { - if( !it.goes_bad() ) { - continue; - } - // make a copy of the item so we can play with its rot values - item it_copy = it; - it_copy.mod_rot( -rot_points ); - max_relative_rot = std::max( max_relative_rot, it_copy.get_relative_rot() ); - } - // Set up the new item, and assign an inventory letter if available std::vector newits = making.create_results( batch_size ); @@ -690,6 +689,7 @@ void player::complete_craft() // messages, learning of recipe, food spoilage calculation only once if( first ) { first = false; + // TODO: reconsider recipe memorization if( knows_recipe( &making ) ) { add_msg( _( "You craft %s from memory." ), newit.type_name( 1 ).c_str() ); } else { @@ -736,8 +736,8 @@ void player::complete_craft() // if a component item has "cooks_like" it will be replaced by that item as a component for( item &comp : used ) { // only comestibles have cooks_like. any other type of item will throw an exception, so filter those out - if( comp.is_comestible() && !comp.type->comestible->cooks_like.empty() ) { - comp = item( comp.type->comestible->cooks_like, comp.birthday(), comp.charges ); + if( comp.is_comestible() && !comp.get_comestible()->cooks_like.empty() ) { + comp = item( comp.get_comestible()->cooks_like, comp.birthday(), comp.charges ); } } // byproducts get stored as a "component" but with a byproduct flag for consumption purposes @@ -755,12 +755,12 @@ void player::complete_craft() } if( newit.goes_bad() ) { - newit.set_relative_rot( max_relative_rot ); + newit.set_relative_rot( relative_rot ); } else { if( newit.is_container() ) { for( item &in : newit.contents ) { if( in.goes_bad() ) { - in.set_relative_rot( max_relative_rot ); + in.set_relative_rot( relative_rot ); } } } @@ -783,14 +783,14 @@ void player::complete_craft() } finalize_crafted_item( newit ); - set_item_inventory( newit ); + set_item_inventory( *this, newit ); } if( making.has_byproducts() ) { std::vector bps = making.create_byproducts( batch_size ); for( auto &bp : bps ) { if( bp.goes_bad() ) { - bp.set_relative_rot( max_relative_rot ); + bp.set_relative_rot( relative_rot ); } if( bp.is_food() ) { if( should_heat ) { @@ -801,7 +801,7 @@ void player::complete_craft() } } finalize_crafted_item( bp ); - set_item_inventory( bp ); + set_item_inventory( *this, bp ); } } @@ -1474,7 +1474,7 @@ void player::complete_disassemble( int item_pos, const tripoint &loc, // If the components aren't empty, we want items exactly identical to them // Even if the best-fit recipe does not involve those items - std::vector components = dis_item.components; + std::list components = dis_item.components; // If the components are empty, item is the default kind and made of default components if( components.empty() ) { const bool uncraft_liquids_contained = dis.has_flag( "UNCRAFT_LIQUIDS_CONTAINED" ); @@ -1543,7 +1543,7 @@ void player::complete_disassemble( int item_pos, const tripoint &loc, act_item.item_tags.insert( "FILTHY" ); } - for( item::t_item_vector::iterator a = dis_item.components.begin(); a != dis_item.components.end(); + for( std::list::iterator a = dis_item.components.begin(); a != dis_item.components.end(); ++a ) { if( a->type == newit.type ) { act_item = *a; diff --git a/src/dump.cpp b/src/dump.cpp index f34c311e40957..6fb49519fdcbf 100644 --- a/src/dump.cpp +++ b/src/dump.cpp @@ -131,9 +131,9 @@ bool game::dump_stats( const std::string &what, dump_mode mode, r.push_back( to_string( obj.volume() / units::legacy_volume_factor ) ); r.push_back( to_string( to_gram( obj.weight() ) ) ); r.push_back( to_string( obj.type->stack_size ) ); - r.push_back( to_string( obj.type->comestible->get_calories() ) ); - r.push_back( to_string( obj.type->comestible->quench ) ); - r.push_back( to_string( obj.type->comestible->healthy ) ); + r.push_back( to_string( obj.get_comestible()->get_calories() ) ); + r.push_back( to_string( obj.get_comestible()->quench ) ); + r.push_back( to_string( obj.get_comestible()->healthy ) ); auto vits = g->u.vitamins_from( obj ); for( const auto &v : vitamin::all() ) { r.push_back( to_string( vits[ v.first ] ) ); diff --git a/src/faction_camp.cpp b/src/faction_camp.cpp index c0b19eb9cf240..1f99aa2f93ed1 100644 --- a/src/faction_camp.cpp +++ b/src/faction_camp.cpp @@ -3434,7 +3434,7 @@ bool basecamp::distribute_food() g->m.add_item_or_charges( litter_spread, i, false ); i = comest; } - if( i.is_comestible() && ( i.rotten() || i.type->comestible->fun < -6 ) ) { + if( i.is_comestible() && ( i.rotten() || i.get_comestible()->fun < -6 ) ) { keep_me.push_back( i ); } else if( i.is_food() ) { double rot_multip; @@ -3446,7 +3446,7 @@ bool basecamp::distribute_food() } else { rot_multip = quick_rot; } - total += i.type->comestible->get_calories() * rot_multip * i.count(); + total += i.get_comestible()->get_calories() * rot_multip * i.count(); } else { keep_me.push_back( i ); } diff --git a/src/game_inventory.cpp b/src/game_inventory.cpp index 814a12dcb4910..b6c22c757be3f 100644 --- a/src/game_inventory.cpp +++ b/src/game_inventory.cpp @@ -500,6 +500,7 @@ class comestible_inventory_preset : public inventory_selector_preset const islot_comestible &get_edible_comestible( const item &it ) const { if( it.is_comestible() && p.can_eat( it ).success() ) { + // Ok since can_eat() returns false if is_craft() is true return *it.type->comestible; } static const islot_comestible dummy {}; diff --git a/src/iexamine.cpp b/src/iexamine.cpp index d3dd6bef1c22b..680ab27a1c8dd 100644 --- a/src/iexamine.cpp +++ b/src/iexamine.cpp @@ -4148,11 +4148,11 @@ void smoker_finalize( player &, const tripoint &examp, const time_point &start_t for( size_t i = 0; i < items.size(); i++ ) { auto &item_it = items[i]; - if( item_it.type->comestible ) { - if( item_it.type->comestible->smoking_result.empty() ) { + if( item_it.get_comestible() ) { + if( item_it.get_comestible()->smoking_result.empty() ) { item_it.unset_flag( "SMOKING" ); } else { - item result( item_it.type->comestible->smoking_result, start_time + 6_hours, item_it.charges ); + item result( item_it.get_comestible()->smoking_result, start_time + 6_hours, item_it.charges ); // Set flag to tell set_relative_rot() to calc from bday not now result.set_flag( "SMOKING_RESULT" ); diff --git a/src/item.cpp b/src/item.cpp index 7b7f64728ec6f..75e613c47efdc 100644 --- a/src/item.cpp +++ b/src/item.cpp @@ -171,7 +171,7 @@ item::item( const itype *type, time_point turn, long qty ) : type( type ), bday( emplace_back( type->magazine->default_ammo, calendar::turn, type->magazine->count ); } - } else if( type->comestible ) { + } else if( get_comestible() ) { active = is_food(); last_temp_check = bday; @@ -209,6 +209,47 @@ item::item( const itype *type, time_point turn, solitary_tag ) item::item( const itype_id &id, time_point turn, solitary_tag tag ) : item( find_type( id ), turn, tag ) {} +static const item *get_most_rotten_component( const item &craft ) +{ + const item *most_rotten = nullptr; + for( const item &it : craft.components ) { + if( it.goes_bad() ) { + if( !most_rotten || it.get_relative_rot() > most_rotten->get_relative_rot() ) { + most_rotten = ⁢ + } + } + } + return most_rotten; +} + +item::item( const recipe *rec, long qty, std::list items ) + : item( "craft", calendar::turn, qty ) +{ + making = rec; + components = items; + + if( is_food() ) { + active = true; + last_temp_check = bday; + if( goes_bad() ) { + set_relative_rot( get_most_rotten_component( *this )->get_relative_rot() ); + } + } + + for( const item &it : components ) { + if( it.has_flag( "HIDDEN_POISON" ) ) { + set_flag( "HIDDEN_POISON" ); + } + if( it.has_flag( "HIDDEN_HALLU" ) ) { + set_flag( "HIDDEN_HALLU" ); + } + if( it.is_filthy() ) { + set_flag( "FILTHY" ); + } + } + +} + item item::make_corpse( const mtype_id &mt, time_point turn, const std::string &name ) { if( !mt.is_valid() ) { @@ -579,9 +620,9 @@ bool item::stacks_with( const item &rhs, bool check_components ) const // Stack items that fall into the same "bucket" of freshness. // Distant buckets are larger than near ones. std::pair my_clipped_time_to_rot = - clipped_time( type->comestible->spoils - rot ); + clipped_time( get_comestible()->spoils - rot ); std::pair other_clipped_time_to_rot = - clipped_time( rhs.type->comestible->spoils - rhs.rot ); + clipped_time( rhs.get_comestible()->spoils - rhs.rot ); if( my_clipped_time_to_rot != other_clipped_time_to_rot ) { return false; } @@ -794,7 +835,7 @@ std::string get_freshness_description( const item &food_item ) // can guess its age as one of {quite fresh,midlife,past midlife,old soon}, and also // guess about how long until it spoils. const double rot_progress = food_item.get_relative_rot(); - const time_duration shelf_life = food_item.type->comestible->spoils; + const time_duration shelf_life = food_item.get_comestible()->spoils; time_duration time_left = shelf_life - ( shelf_life * rot_progress ); // Correct for an estimate that exceeds shelf life -- this happens especially with @@ -1017,7 +1058,7 @@ std::string item::info( std::vector &info, const iteminfo_query *parts to_turns( food->rot ) ) ); info.push_back( iteminfo( "BASE", space + _( "max rot: " ), "", iteminfo::lower_is_better, - to_turns( food->type->comestible->spoils ) ) ); + to_turns( food->get_comestible()->spoils ) ) ); info.push_back( iteminfo( "BASE", _( "last rot: " ), "", iteminfo::lower_is_better, to_turn( food->last_rot_check ) ) ); @@ -1031,11 +1072,11 @@ std::string item::info( std::vector &info, const iteminfo_query *parts info.push_back( iteminfo( "BASE", _( "Spec ener: " ), "", iteminfo::lower_is_better, food->specific_energy ) ); info.push_back( iteminfo( "BASE", _( "Spec heat lq: " ), "", iteminfo::lower_is_better, - 1000 * food->type->comestible->specific_heat_liquid ) ); + 1000 * food->get_comestible()->specific_heat_liquid ) ); info.push_back( iteminfo( "BASE", _( "Spec heat sld: " ), "", iteminfo::lower_is_better, - 1000 * food->type->comestible->specific_heat_solid ) ); + 1000 * food->get_comestible()->specific_heat_solid ) ); info.push_back( iteminfo( "BASE", _( "latent heat: " ), "", iteminfo::lower_is_better, - food->type->comestible->latent_heat ) ); + food->get_comestible()->latent_heat ) ); } } info.push_back( iteminfo( "BASE", _( "burn: " ), "", iteminfo::lower_is_better, @@ -1050,7 +1091,7 @@ std::string item::info( std::vector &info, const iteminfo_query *parts med_item = &contents.front(); } if( med_item != nullptr ) { - const auto &med_com = med_item->type->comestible; + const auto &med_com = med_item->get_comestible(); if( med_com->quench != 0 && parts->test( iteminfo_parts::MED_QUENCH ) ) { info.push_back( iteminfo( "MED", _( "Quench: " ), med_com->quench ) ); } @@ -1083,7 +1124,7 @@ std::string item::info( std::vector &info, const iteminfo_query *parts food_item = &contents.front(); } if( food_item != nullptr ) { - if( g->u.kcal_for( *food_item ) != 0 || food_item->type->comestible->quench != 0 ) { + if( g->u.kcal_for( *food_item ) != 0 || food_item->get_comestible()->quench != 0 ) { if( parts->test( iteminfo_parts::FOOD_NUTRITION ) ) { auto value = g->u.kcal_for( *food_item ); info.push_back( iteminfo( "FOOD", _( "Calories (kcal): " ), @@ -1091,11 +1132,11 @@ std::string item::info( std::vector &info, const iteminfo_query *parts } if( parts->test( iteminfo_parts::FOOD_QUENCH ) ) { info.push_back( iteminfo( "FOOD", space + _( "Quench: " ), - food_item->type->comestible->quench ) ); + food_item->get_comestible()->quench ) ); } } - if( food_item->type->comestible->fun != 0 && parts->test( iteminfo_parts::FOOD_JOY ) ) { + if( food_item->get_comestible()->fun != 0 && parts->test( iteminfo_parts::FOOD_JOY ) ) { info.push_back( iteminfo( "FOOD", _( "Enjoyability: " ), g->u.fun_for( *food_item ).first ) ); } @@ -1151,7 +1192,7 @@ std::string item::info( std::vector &info, const iteminfo_query *parts } if( food_item->goes_bad() && parts->test( iteminfo_parts::FOOD_ROT ) ) { - const std::string rot_time = to_string_clipped( food_item->type->comestible->spoils ); + const std::string rot_time = to_string_clipped( food_item->get_comestible()->spoils ); info.emplace_back( "DESCRIPTION", string_format( _( "* This food is perishable, and at room temperature has an estimated nominal shelf life of %s." ), @@ -1995,8 +2036,13 @@ std::string item::info( std::vector &info, const iteminfo_query *parts } if( !components.empty() && parts->test( iteminfo_parts::DESCRIPTION_COMPONENTS_MADEFROM ) ) { - info.push_back( iteminfo( "DESCRIPTION", string_format( _( "Made from: %s" ), - _( components_to_string().c_str() ) ) ) ); + if( is_craft() ) { + info.push_back( iteminfo( "DESCRIPTION", string_format( _( "Using: %s" ), + _( components_to_string().c_str() ) ) ) ); + } else { + info.push_back( iteminfo( "DESCRIPTION", string_format( _( "Made from: %s" ), + _( components_to_string().c_str() ) ) ) ); + } } else { const auto &dis = recipe_dictionary::get_uncraft( typeId() ); const auto &req = dis.disassembly_requirements(); @@ -2066,7 +2112,15 @@ std::string item::info( std::vector &info, const iteminfo_query *parts } else if( idescription != item_vars.end() ) { info.push_back( iteminfo( "DESCRIPTION", idescription->second ) ); } else { - info.push_back( iteminfo( "DESCRIPTION", _( type->description.c_str() ) ) ); + if( is_craft() ) { + const std::string desc = _( "This is an in progress %s. It is %d percent complete." ); + const int percent_progress = 100 * item_counter / making->time; + info.push_back( iteminfo( "DESCRIPTION", string_format( desc, + making->result_name(), + percent_progress ) ) ); + } else { + info.push_back( iteminfo( "DESCRIPTION", _( type->description.c_str() ) ) ); + } } } auto all_techniques = type->techniques; @@ -2591,7 +2645,9 @@ nc_color item::color_in_inventory() const ret = c_brown; } else if( has_flag( "LEAK_DAM" ) && has_flag( "RADIOACTIVE" ) && damage() > 0 ) { ret = c_light_green; - } else if( active && !is_food() && !is_food_container() ) { // Active items show up as yellow + + } else if( active && !is_food() && !is_food_container() ) { + // Active items show up as yellow ret = c_yellow; } else if( is_food() || is_food_container() ) { const bool preserves = type->container && type->container->preserves; @@ -2916,6 +2972,16 @@ std::string item::tname( unsigned int quantity, bool with_prefix, unsigned int t ret << label( quantity ); ret << "+1"; maintext = ret.str(); + } else if( is_craft() ) { + ret.str( "" ); + const std::string name = _( "in progress %s" ); + ret << string_format( name, making->result_name() ); + if( charges > 1 ) { + ret << " (" << charges << ")"; + } + const int percent_progress = 100 * item_counter / making->time; + ret << " (" << percent_progress << "%)"; + maintext = ret.str(); } else if( contents.size() == 1 ) { const item &contents_item = contents.front(); if( contents_item.made_of( LIQUID ) || contents_item.is_food() ) { @@ -3185,7 +3251,17 @@ units::mass item::weight( bool include_contents ) const return 0_gram; } - units::mass ret = units::from_gram( get_var( "weight", to_gram( type->weight ) ) ); + if( is_craft() ) { + units::mass ret = 0_gram; + for( auto it : components ) { + ret += it.weight(); + } + return ret; + } + + units::mass ret = 0_gram; + ret = units::from_gram( get_var( "weight", to_gram( type->weight ) ) ); + if( has_flag( "REDUCED_WEIGHT" ) ) { ret *= 0.75; } @@ -3272,6 +3348,14 @@ units::volume item::base_volume() const return corpse_volume( corpse ); } + if( is_craft() ) { + units::volume ret = 0_ml; + for( auto it : components ) { + ret += it.base_volume(); + } + return ret; + } + if( count_by_charges() ) { if( type->volume % type->stack_size == 0_ml ) { return type->volume / type->stack_size; @@ -3293,6 +3377,14 @@ units::volume item::volume( bool integral ) const return corpse_volume( corpse ); } + if( is_craft() ) { + units::volume ret = 0_ml; + for( auto it : components ) { + ret += it.volume(); + } + return ret; + } + const int local_volume = get_var( "volume", -1 ); units::volume ret; if( local_volume >= 0 ) { @@ -3616,18 +3708,18 @@ std::set item::get_techniques() const bool item::goes_bad() const { - return is_food() && type->comestible->spoils != 0_turns; + return is_food() && get_comestible()->spoils != 0_turns; } double item::get_relative_rot() const { - return goes_bad() ? rot / type->comestible->spoils : 0; + return goes_bad() ? rot / get_comestible()->spoils : 0; } void item::set_relative_rot( double val ) { if( goes_bad() ) { - rot = type->comestible->spoils * val; + rot = get_comestible()->spoils * val; // calc_rot uses last_rot_check (when it's not time_of_cataclysm) instead of bday. // this makes sure the rotting starts from now, not from bday. // if this item is the result of smoking don't do this, we want to start from bday. @@ -3653,10 +3745,10 @@ int item::spoilage_sort_order() } if( subject->goes_bad() ) { - return to_turns( subject->type->comestible->spoils - subject->rot ); + return to_turns( subject->get_comestible()->spoils - subject->rot ); } - if( subject->type->comestible ) { + if( subject->get_comestible() ) { if( subject->get_category().id() == "food" ) { return bottom - 3; } else if( subject->get_category().id() == "drugs" ) { @@ -3702,7 +3794,7 @@ void item::calc_rot( const tripoint &location ) // positive = food was produced some time before calendar::start and/or bad storage // negative = food was stored in good conditions before calendar::start if( since <= calendar::start && goes_bad() ) { - time_duration spoil_variation = type->comestible->spoils * 0.2f; + time_duration spoil_variation = get_comestible()->spoils * 0.2f; rot += factor * rng( -spoil_variation, spoil_variation ); } @@ -4463,18 +4555,18 @@ bool item::is_ammo() const bool item::is_comestible() const { - return type->comestible.has_value(); + return get_comestible().has_value(); } bool item::is_food() const { - return is_comestible() && ( type->comestible->comesttype == "FOOD" || - type->comestible->comesttype == "DRINK" ); + return is_comestible() && ( get_comestible()->comesttype == "FOOD" || + get_comestible()->comesttype == "DRINK" ); } bool item::is_medication() const { - return is_comestible() && type->comestible->comesttype == "MED"; + return is_comestible() && get_comestible()->comesttype == "MED"; } bool item::is_brewable() const @@ -4484,7 +4576,8 @@ bool item::is_brewable() const bool item::is_food_container() const { - return !contents.empty() && contents.front().is_food(); + return ( !contents.empty() && contents.front().is_food() ) || ( is_craft() && + making->create_result().is_food_container() ); } bool item::is_med_container() const @@ -4745,6 +4838,11 @@ bool item::is_salvageable() const return !has_flag( "NO_SALVAGE" ); } +bool item::is_craft() const +{ + return making != nullptr; +} + bool item::is_funnel_container( units::volume &bigger_than ) const { if( !is_bucket() && !is_watertight_container() ) { @@ -6249,10 +6347,10 @@ bool item::allow_crafting_component() const void item::set_item_specific_energy( const float new_specific_energy ) { - const float specific_heat_liquid = type->comestible->specific_heat_liquid; // J/g K - const float specific_heat_solid = type->comestible->specific_heat_solid; // J/g K - const float latent_heat = type->comestible->latent_heat; // J/kg - const float freezing_temperature = temp_to_kelvin( type->comestible->freeze_point ); // K + const float specific_heat_liquid = get_comestible()->specific_heat_liquid; // J/g K + const float specific_heat_solid = get_comestible()->specific_heat_solid; // J/g K + const float latent_heat = get_comestible()->latent_heat; // J/kg + const float freezing_temperature = temp_to_kelvin( get_comestible()->freeze_point ); // K const float completely_frozen_specific_energy = specific_heat_solid * freezing_temperature; // Energy that the item would have if it was completely solid at freezing temperature const float completely_liquid_specific_energy = completely_frozen_specific_energy + @@ -6299,7 +6397,7 @@ void item::set_item_specific_energy( const float new_specific_energy ) item_tags.insert( "FROZEN" ); current_phase = SOLID; // If below freezing temp AND the food may have parasites AND food does not have "NO_PARASITES" tag then add the "NO_PARASITES" tag. - if( new_item_temperature < freezing_temperature && type->comestible->parasites > 0 ) { + if( new_item_temperature < freezing_temperature && get_comestible()->parasites > 0 ) { if( !( item_tags.count( "NO_PARASITES" ) ) ) { item_tags.insert( "NO_PARASITES" ); } @@ -6314,10 +6412,10 @@ void item::set_item_specific_energy( const float new_specific_energy ) float item::get_specific_energy_from_temperature( const float new_temperature ) { - const float specific_heat_liquid = type->comestible->specific_heat_liquid; // J/g K - const float specific_heat_solid = type->comestible->specific_heat_solid; // J/g K - const float latent_heat = type->comestible->latent_heat; // J/kg - const float freezing_temperature = temp_to_kelvin( type->comestible->freeze_point ); // K + const float specific_heat_liquid = get_comestible()->specific_heat_liquid; // J/g K + const float specific_heat_solid = get_comestible()->specific_heat_solid; // J/g K + const float latent_heat = get_comestible()->latent_heat; // J/kg + const float freezing_temperature = temp_to_kelvin( get_comestible()->freeze_point ); // K const float completely_frozen_energy = specific_heat_solid * freezing_temperature; // Energy that the item would have if it was completely solid at freezing temperature const float completely_liquid_energy = completely_frozen_energy + @@ -6335,9 +6433,9 @@ float item::get_specific_energy_from_temperature( const float new_temperature ) void item::set_item_temperature( float new_temperature ) { - const float freezing_temperature = temp_to_kelvin( type->comestible->freeze_point ); // K - const float specific_heat_solid = type->comestible->specific_heat_solid; // J/g K - const float latent_heat = type->comestible->latent_heat; // J/kg + const float freezing_temperature = temp_to_kelvin( get_comestible()->freeze_point ); // K + const float specific_heat_solid = get_comestible()->specific_heat_solid; // J/g K + const float latent_heat = get_comestible()->latent_heat; // J/kg float new_specific_energy = get_specific_energy_from_temperature( new_temperature ); float freeze_percentage = 0; @@ -6376,7 +6474,7 @@ void item::set_item_temperature( float new_temperature ) item_tags.insert( "FROZEN" ); current_phase = SOLID; // If below freezing temp AND the food may have parasites AND food does not have "NO_PARASITES" tag then add the "NO_PARASITES" tag. - if( new_temperature < freezing_temperature && type->comestible->parasites > 0 ) { + if( new_temperature < freezing_temperature && get_comestible()->parasites > 0 ) { if( !( item_tags.count( "NO_PARASITES" ) ) ) { item_tags.insert( "NO_PARASITES" ); } @@ -6706,7 +6804,7 @@ bool item::needs_processing() const { return active || has_flag( "RADIO_ACTIVATION" ) || ( is_container() && !contents.empty() && contents.front().needs_processing() ) || - is_artifact() || ( is_food() ); + is_artifact() || is_food(); } int item::processing_speed() const @@ -6776,10 +6874,10 @@ void item::calc_temp( const int temp, const float insulation, const time_duratio // temperature = item temperature (10e-5 K). Stored in the item const float conductivity_term = 0.046 * std::pow( to_milliliter( volume() ), 2.0 / 3.0 ) / insulation; - const float specific_heat_liquid = type->comestible->specific_heat_liquid; - const float specific_heat_solid = type->comestible->specific_heat_solid; - const float latent_heat = type->comestible->latent_heat; - const float freezing_temperature = temp_to_kelvin( type->comestible->freeze_point ); // K + const float specific_heat_liquid = get_comestible()->specific_heat_liquid; + const float specific_heat_solid = get_comestible()->specific_heat_solid; + const float latent_heat = get_comestible()->latent_heat; + const float freezing_temperature = temp_to_kelvin( get_comestible()->freeze_point ); // K const float completely_frozen_specific_energy = specific_heat_solid * freezing_temperature; // Energy that the item would have if it was completely solid at freezing temperature const float completely_liquid_specific_energy = completely_frozen_specific_energy + @@ -6913,7 +7011,7 @@ void item::calc_temp( const int temp, const float insulation, const time_duratio item_tags.insert( "FROZEN" ); current_phase = SOLID; // If below freezing temp AND the food may have parasites AND food does not have "NO_PARASITES" tag then add the "NO_PARASITES" tag. - if( new_item_temperature < freezing_temperature && type->comestible->parasites > 0 ) { + if( new_item_temperature < freezing_temperature && get_comestible()->parasites > 0 ) { if( !( item_tags.count( "NO_PARASITES" ) ) ) { item_tags.insert( "NO_PARASITES" ); } @@ -7384,7 +7482,7 @@ bool item::process( player *carrier, const tripoint &pos, bool activate, int tem if( has_flag( "FAKE_SMOKE" ) && process_fake_smoke( carrier, pos ) ) { return true; } - if( is_food() && process_food( carrier, pos, temp, insulation ) ) { + if( is_food() && process_food( carrier, pos, temp, insulation ) ) { return true; } if( is_corpse() && process_corpse( carrier, pos ) ) { @@ -7757,3 +7855,18 @@ std::vector item::get_uncraft_components() const } return ret; } + +const recipe &item::get_making() const +{ + if( !making ) { + debugmsg( "'%s' is not a craft or has a null recipe", tname() ); + return recipe().ident().obj(); + } + return *making; +} + +const cata::optional &item::get_comestible() const +{ + return is_craft() ? find_type( making->result() )->comestible : + type->comestible; +} diff --git a/src/item.h b/src/item.h index 44ef43076daa5..f2e767c2f0c21 100644 --- a/src/item.h +++ b/src/item.h @@ -42,6 +42,7 @@ class player; class npc; class recipe; struct itype; +struct islot_comestible; struct mtype; using mtype_id = string_id; using bodytype_id = std::string; @@ -63,6 +64,7 @@ class ma_technique; using matec_id = string_id; struct point; struct tripoint; +using recipe_id = string_id; class Skill; using skill_id = string_id; class fault; @@ -186,6 +188,9 @@ class item : public visitable item( const itype_id &id, time_point turn, solitary_tag ); item( const itype *type, time_point turn, solitary_tag ); + /** For constructing in-progress crafts */ + item( const recipe *rec, long qty, std::list items ); + /** * Filter converting this instance to another type preserving all other aspects * @param new_type the type id to convert to @@ -1009,6 +1014,7 @@ class item : public visitable bool is_book() const; bool is_map() const; bool is_salvageable() const; + bool is_craft() const; bool is_tool() const; bool is_tool_reversible() const; @@ -1806,6 +1812,10 @@ class item : public visitable int get_min_str() const; + const recipe &get_making() const; + + const cata::optional &get_comestible() const; + private: /** * Calculate the thermal energy and temperature change of the item @@ -1845,11 +1855,10 @@ class item : public visitable public: static const long INFINITE_CHARGES; - typedef std::vector t_item_vector; const itype *type; std::list contents; - t_item_vector components; + std::list components; /** What faults (if any) currently apply to this item */ std::set faults; std::set item_tags; // generic item specific flags @@ -1860,6 +1869,7 @@ class item : public visitable const mtype *corpse = nullptr; std::string corpse_name; // Name of the late lamented std::set techniques; // item specific techniques + const recipe *making = nullptr; // Only for in-progress crafts public: long charges; diff --git a/src/item_factory.cpp b/src/item_factory.cpp index 4a2559b34ff4e..a9b030ea3961b 100644 --- a/src/item_factory.cpp +++ b/src/item_factory.cpp @@ -614,6 +614,7 @@ void Item_factory::init() add_iuse( "DIVE_TANK", &iuse::dive_tank ); add_iuse( "DIRECTIONAL_ANTENNA", &iuse::directional_antenna ); add_iuse( "DISASSEMBLE", &iuse::disassemble ); + add_iuse( "CRAFT", &iuse::craft ); add_iuse( "DOGFOOD", &iuse::dogfood ); add_iuse( "DOG_WHISTLE", &iuse::dog_whistle ); add_iuse( "DOLLCHAT", &iuse::talking_doll ); diff --git a/src/iuse.cpp b/src/iuse.cpp index 16bf5b43196c2..2929fc9d00944 100644 --- a/src/iuse.cpp +++ b/src/iuse.cpp @@ -287,14 +287,14 @@ int iuse::xanax( player *p, item *it, bool, const tripoint & ) int iuse::caff( player *p, item *it, bool, const tripoint & ) { - p->mod_fatigue( -( it->type->comestible ? it->type->comestible->stim : 0 ) * 3 ); + p->mod_fatigue( -( it->get_comestible() ? it->get_comestible()->stim : 0 ) * 3 ); return it->type->charges_to_use(); } int iuse::atomic_caff( player *p, item *it, bool, const tripoint & ) { p->add_msg_if_player( m_good, _( "Wow! This %s has a kick." ), it->tname().c_str() ); - p->mod_fatigue( -( it->type->comestible ? it->type->comestible->stim : 0 ) * 12 ); + p->mod_fatigue( -( it->get_comestible() ? it->get_comestible()->stim : 0 ) * 12 ); p->irradiate( 8, true ); return it->type->charges_to_use(); } @@ -316,9 +316,9 @@ int alcohol( player &p, const item &it, const int strength ) 6_turns, 10_turns, 10_turns ) * p.str_max ); // Metabolizing the booze improves the nutritional value; // might not be healthy, and still causes Thirst problems, though - p.mod_hunger( -( abs( it.type->comestible ? it.type->comestible->stim : 0 ) ) ); + p.mod_hunger( -( abs( it.get_comestible() ? it.get_comestible()->stim : 0 ) ) ); // Metabolizing it cancels out the depressant - p.stim += abs( it.type->comestible ? it.type->comestible->stim : 0 ); + p.stim += abs( it.get_comestible() ? it.get_comestible()->stim : 0 ); } else if( p.has_trait( trait_TOLERANCE ) ) { duration -= alc_strength( strength, 12_minutes, 30_minutes, 45_minutes ); } else if( p.has_trait( trait_LIGHTWEIGHT ) ) { @@ -885,12 +885,12 @@ int iuse::blech( player *p, item *it, bool, const tripoint & ) //reverse the harmful values of drinking this acid. double multiplier = -1; p->mod_hunger( -p->nutrition_for( *it ) * multiplier ); - p->mod_thirst( -it->type->comestible->quench * multiplier ); + p->mod_thirst( -it->get_comestible()->quench * multiplier ); p->mod_thirst( -20 ); //acidproof people can drink acids like diluted water. p->mod_stomach_water( 20 ); - p->mod_healthy_mod( it->type->comestible->healthy * multiplier, - it->type->comestible->healthy * multiplier ); - p->add_morale( MORALE_FOOD_BAD, it->type->comestible->fun * multiplier, 60, 1_hours, 30_minutes, + p->mod_healthy_mod( it->get_comestible()->healthy * multiplier, + it->get_comestible()->healthy * multiplier ); + p->add_morale( MORALE_FOOD_BAD, it->get_comestible()->fun * multiplier, 60, 1_hours, 30_minutes, false, it->type ); } else { p->add_msg_if_player( m_bad, _( "Blech, that burns your throat!" ) ); @@ -915,9 +915,9 @@ int iuse::plantblech( player *p, item *it, bool, const tripoint &pos ) //reverses the harmful values of drinking fertilizer p->mod_hunger( p->nutrition_for( *it ) * multiplier ); - p->mod_thirst( -it->type->comestible->quench * multiplier ); - p->mod_healthy_mod( it->type->comestible->healthy * multiplier, - it->type->comestible->healthy * multiplier ); + p->mod_thirst( -it->get_comestible()->quench * multiplier ); + p->mod_healthy_mod( it->get_comestible()->healthy * multiplier, + it->get_comestible()->healthy * multiplier ); p->add_morale( MORALE_FOOD_GOOD, -10 * multiplier, 60, 1_hours, 30_minutes, false, it->type ); return it->type->charges_to_use(); } else { @@ -8062,6 +8062,22 @@ int iuse::panacea( player *p, item *it, bool, const tripoint & ) return it->type->charges_to_use(); } +int iuse::craft( player *p, item *it, bool, const tripoint & ) +{ + int pos = p->get_item_position( it ); + + if( pos != INT_MIN ) { + p->add_msg_player_or_npc( + string_format( pgettext( "in progress craft", "You start working on the %s" ), it->tname() ), + string_format( pgettext( "in progress craft", " starts working on the %s" ), it->tname() + ) ); + p->assign_activity( activity_id( "ACT_CRAFT" ) ); + p->activity.targets.push_back( item_location( *p, it ) ); + p->activity.values.push_back( 0 ); // Not a long craft + } + return 0; +} + int iuse::disassemble( player *p, item *it, bool, const tripoint & ) { int pos = p->get_item_position( it ); diff --git a/src/iuse.h b/src/iuse.h index ce2a27f74715e..2078506b0ea63 100644 --- a/src/iuse.h +++ b/src/iuse.h @@ -209,6 +209,8 @@ class iuse int remoteveh( player *, item *, bool, const tripoint & ); + int craft( player *, item *, bool, const tripoint & ); + int disassemble( player *, item *, bool, const tripoint & ); // ARTIFACTS diff --git a/src/map.cpp b/src/map.cpp index 20695a2f00c4a..ec41f45e80d4a 100644 --- a/src/map.cpp +++ b/src/map.cpp @@ -6632,7 +6632,7 @@ void map::rotten_item_spawn( const item &item, const tripoint &pnt ) if( g->critter_at( pnt ) != nullptr ) { return; } - auto &comest = item.type->comestible; + const auto &comest = item.get_comestible(); mongroup_id mgroup = comest->rot_spawn; if( mgroup == "GROUP_NULL" ) { return; diff --git a/src/monattack.cpp b/src/monattack.cpp index 3ea741b30bc19..f77f422128a5c 100644 --- a/src/monattack.cpp +++ b/src/monattack.cpp @@ -239,7 +239,7 @@ bool mattack::eat_food( monster *z ) auto items = g->m.i_at( p ); for( auto &item : items ) { //Fun limit prevents scavengers from eating feces - if( !item.is_food() || item.type->comestible->fun < -20 ) { + if( !item.is_food() || item.get_comestible()->fun < -20 ) { continue; } //Don't eat own eggs diff --git a/src/npc.cpp b/src/npc.cpp index b3816d6a0ee7e..6604e57394aa2 100644 --- a/src/npc.cpp +++ b/src/npc.cpp @@ -1386,10 +1386,10 @@ void npc::decide_needs() item inventory_item = i->front(); if( inventory_item.is_food( ) ) { needrank[ need_food ] += nutrition_for( inventory_item ) / 4; - needrank[ need_drink ] += inventory_item.type->comestible->quench / 4; + needrank[ need_drink ] += inventory_item.get_comestible()->quench / 4; } else if( inventory_item.is_food_container() ) { needrank[ need_food ] += nutrition_for( inventory_item.contents.front() ) / 4; - needrank[ need_drink ] += inventory_item.contents.front().type->comestible->quench / 4; + needrank[ need_drink ] += inventory_item.contents.front().get_comestible()->quench / 4; } } needs.clear(); @@ -1550,14 +1550,14 @@ int npc::value( const item &it, int market_price ) const if( it.is_food() ) { int comestval = 0; - if( nutrition_for( it ) > 0 || it.type->comestible->quench > 0 ) { + if( nutrition_for( it ) > 0 || it.get_comestible()->quench > 0 ) { comestval++; } if( get_hunger() > 40 ) { comestval += ( nutrition_for( it ) + get_hunger() - 40 ) / 6; } if( get_thirst() > 40 ) { - comestval += ( it.type->comestible->quench + get_thirst() - 40 ) / 4; + comestval += ( it.get_comestible()->quench + get_thirst() - 40 ) / 4; } if( comestval > 0 && will_eat( it ).success() ) { ret += comestval; @@ -1593,11 +1593,11 @@ int npc::value( const item &it, int market_price ) const // TODO: Artifact hunting from relevant factions // ALSO TODO: Bionics hunting from relevant factions - if( fac_has_job( FACJOB_DRUGS ) && it.is_food() && it.type->comestible->addict >= 5 ) { + if( fac_has_job( FACJOB_DRUGS ) && it.is_food() && it.get_comestible()->addict >= 5 ) { ret += 10; } - if( fac_has_job( FACJOB_DOCTORS ) && it.is_food() && it.type->comestible->comesttype == "MED" ) { + if( fac_has_job( FACJOB_DOCTORS ) && it.is_food() && it.get_comestible()->comesttype == "MED" ) { ret += 10; } @@ -2389,8 +2389,8 @@ bool npc::will_accept_from_player( const item &it ) const return false; } - if( const auto &comest = it.is_container() ? it.get_contained().type->comestible : - it.type->comestible ) { + if( const auto &comest = it.is_container() ? it.get_contained().get_comestible() : + it.get_comestible() ) { if( comest->fun < 0 || it.poison > 0 ) { return false; } diff --git a/src/npcmove.cpp b/src/npcmove.cpp index 9b6cccd18597c..b221abb305f4c 100644 --- a/src/npcmove.cpp +++ b/src/npcmove.cpp @@ -2776,7 +2776,7 @@ void npc::use_painkiller() // Be eaten before it rots (favor soon-to-rot perishables) float rate_food( const item &it, int want_nutr, int want_quench ) { - const auto &food = it.type->comestible; + const auto &food = it.get_comestible(); if( !food ) { return 0.0f; } diff --git a/src/npctalk.cpp b/src/npctalk.cpp index 8b04a4f6ba9af..4998a10078928 100644 --- a/src/npctalk.cpp +++ b/src/npctalk.cpp @@ -2998,7 +2998,7 @@ consumption_result try_consume( npc &p, item &it, std::string &reason ) // TODO: Unify this with 'player::consume_item()' bool consuming_contents = it.is_container() && !it.contents.empty(); item &to_eat = consuming_contents ? it.contents.front() : it; - const auto &comest = to_eat.type->comestible; + const auto &comest = to_eat.get_comestible(); if( !comest ) { // Don't inform the player that we don't want to eat the lighter return REFUSED; diff --git a/src/player.cpp b/src/player.cpp index d3b0a6ebf76f6..2535180be0f53 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -7277,7 +7277,7 @@ bool player::consume_med( item &target ) return false; } - const itype_id tool_type = target.type->comestible->tool; + const itype_id tool_type = target.get_comestible()->tool; const auto req_tool = item::find_type( tool_type ); bool tool_override = false; if( tool_type == "syringe" && has_bionic( bio_syringe ) ) { @@ -7321,7 +7321,7 @@ bool player::consume_item( item &target ) item &comest = get_comestible_from( target ); - if( comest.is_null() ) { + if( comest.is_null() || target.is_craft() ) { add_msg_if_player( m_info, _( "You can't eat your %s." ), target.tname().c_str() ); if( is_npc() ) { debugmsg( "%s tried to eat a %s", name.c_str(), target.tname().c_str() ); @@ -9091,10 +9091,10 @@ void player::use( item_location loc ) } invoke_item( &used, loc.position() ); - } else if( used.is_food() || - used.is_medication() || - used.get_contained().is_food() || - used.get_contained().is_medication() ) { + } else if( !used.is_craft() && ( used.is_food() || + used.is_medication() || + used.get_contained().is_food() || + used.get_contained().is_medication() ) ) { consume( inventory_position ); } else if( used.is_book() ) { @@ -11284,7 +11284,6 @@ void player::assign_activity( const player_activity &act, bool allow_resume ) add_msg_if_player( _( "You resume your task." ) ); activity = backlog.front(); backlog.pop_front(); - activity.resume_with( act ); } else { if( activity ) { backlog.push_front( activity ); diff --git a/src/player.h b/src/player.h index cf00c2a0f0b2b..cfe39e6b7ba80 100644 --- a/src/player.h +++ b/src/player.h @@ -1379,8 +1379,9 @@ class player : public Character void make_all_craft( const recipe_id &id, int batch_size ); std::list consume_components_for_craft( const recipe &making, int batch_size, bool ignore_last = false ); - std::list consume_some_components_for_craft( const recipe &making, int batch_size ); - void complete_craft(); + /** consume components and create an active, in progress craft containing them */ + void start_craft( const recipe &making, int batch_size, bool is_long ); + void complete_craft( item &craft ); /** Returns nearby NPCs ready and willing to help with crafting. */ std::vector get_crafting_helpers() const; int get_num_crafting_helpers( int max ) const; diff --git a/src/player_activity.cpp b/src/player_activity.cpp index a09b9b5eb1800..d7b005a860ff2 100644 --- a/src/player_activity.cpp +++ b/src/player_activity.cpp @@ -158,20 +158,7 @@ bool player_activity::can_resume_with( const player_activity &other, const Chara return false; } - if( id() == activity_id( "ACT_CRAFT" ) || id() == activity_id( "ACT_LONGCRAFT" ) ) { - // The last value is a time stamp, and the last coord is the player - // position. We want to allow either to have changed. - // (This would be much less hacky in the hypothetical future of - // activity_handler_actors). - if( !( values.size() == other.values.size() && - !values.empty() && - std::equal( values.begin(), values.end() - 1, other.values.begin() ) && - coords.size() == other.coords.size() && - !coords.empty() && - std::equal( coords.begin(), coords.end() - 1, other.coords.begin() ) ) ) { - return false; - } - } else if( id() == activity_id( "ACT_CLEAR_RUBBLE" ) ) { + if( id() == activity_id( "ACT_CLEAR_RUBBLE" ) ) { if( other.coords.empty() || other.coords[0] != coords[0] ) { return false; } @@ -213,22 +200,6 @@ bool player_activity::can_resume_with( const player_activity &other, const Chara position == other.position && name == other.name && targets == other.targets; } -void player_activity::resume_with( const player_activity &other ) -{ - if( id() == activity_id( "ACT_CRAFT" ) || id() == activity_id( "ACT_LONGCRAFT" ) ) { - // For crafting actions, we need to update the start turn and position - // to the resumption time values. These are stored in the last - // elements of values and coords respectively. - if( !( !values.empty() && values.size() == other.values.size() && - !coords.empty() && coords.size() == other.coords.size() ) ) { - debugmsg( "Activities incompatible; should not have resumed" ); - return; - } - values.back() = other.values.back(); - coords.back() = other.coords.back(); - } -} - bool player_activity::is_distraction_ignored( distraction_type type ) const { return ignored_distractions.find( type ) != ignored_distractions.end(); diff --git a/src/player_activity.h b/src/player_activity.h index f325b93c1115c..11cf30e1d968d 100644 --- a/src/player_activity.h +++ b/src/player_activity.h @@ -100,13 +100,6 @@ class player_activity * can be resumed instead of starting the other activity. */ bool can_resume_with( const player_activity &other, const Character &who ) const; - /** - * When an old activity A is resumed by a new activity B, normally B is - * discarded and the saved A is simply used in its place. However, - * this will be called on A, passing B as an argument, in case A needs - * to grab any values from B. - */ - void resume_with( const player_activity &other ); bool is_distraction_ignored( distraction_type type ) const; void ignore_distraction( distraction_type type ); diff --git a/src/savegame_json.cpp b/src/savegame_json.cpp index 25cd6d1615fc0..8a4e6c63d357a 100644 --- a/src/savegame_json.cpp +++ b/src/savegame_json.cpp @@ -1809,6 +1809,9 @@ void item::io( Archive &archive ) corpse = &mtype_id( id ).obj(); } }; + const auto load_making = [this]( const std::string & id ) { + making = &recipe_id( id ).obj(); + }; archive.template io( "typeid", type, load_type, []( const itype & i ) { return i.get_id(); @@ -1854,6 +1857,10 @@ void item::io( Archive &archive ) []( const mtype & i ) { return i.id.str(); } ); + archive.template io( "making", making, load_making, + []( const recipe & i ) { + return i.ident().str(); + } ); archive.io( "light", light.luminance, nolight.luminance ); archive.io( "light_width", light.width, nolight.width ); archive.io( "light_dir", light.direction, nolight.direction ); diff --git a/src/savegame_legacy.cpp b/src/savegame_legacy.cpp index 50b013aeabc8e..3e08f4e3abda1 100644 --- a/src/savegame_legacy.cpp +++ b/src/savegame_legacy.cpp @@ -540,7 +540,7 @@ void player_activity::deserialize_legacy_type( int legacy_type, activity_id &des activity_id( "ACT_GAME" ), activity_id( "ACT_WAIT" ), activity_id( "ACT_CRAFT" ), - activity_id( "ACT_LONGCRAFT" ), + activity_id::NULL_ID(), // ACT_LONGCRAFT is deprecated activity_id( "ACT_DISASSEMBLE" ), activity_id( "ACT_BUTCHER" ), activity_id( "ACT_LONGSALVAGE" ), diff --git a/tests/crafting_test.cpp b/tests/crafting_test.cpp index 0a658fd1c785e..4539df20543b6 100644 --- a/tests/crafting_test.cpp +++ b/tests/crafting_test.cpp @@ -425,6 +425,29 @@ static int actually_test_craft( const recipe_id &rid, const std::vector to return turns; } +// Resume the first in progress craft found in the player's inventory +static int resume_craft() +{ + item *craft = g->u.items_with( []( const item & itm ) { + return itm.is_craft(); + } ).front(); + const recipe &rec = craft->get_making(); + set_time( midday ); // Ensure light for crafting + REQUIRE( g->u.morale_crafting_speed_multiplier( rec ) == 1.0 ); + REQUIRE( g->u.lighting_craft_speed_multiplier( rec ) == 1.0 ); + REQUIRE( !g->u.activity ); + g->u.use( g->u.get_item_position( craft ) ); + CHECK( g->u.activity ); + CHECK( g->u.activity.id() == activity_id( "ACT_CRAFT" ) ); + int turns = 0; + while( g->u.activity.id() == activity_id( "ACT_CRAFT" ) ) { + ++turns; + g->u.moves = 100; + g->u.activity.do_turn( g->u ); + } + return turns; +} + static void verify_inventory( const std::vector &has, const std::vector &hasnt ) { @@ -462,11 +485,11 @@ TEST_CASE( "crafting_interruption" ) SECTION( "interrupted_craft" ) { int turns_taken = actually_test_craft( test_recipe, tools, 2 ); CHECK( turns_taken == 3 ); - verify_inventory( { "scrap" }, { "crude_picklock" } ); + verify_inventory( { "craft" }, { "crude_picklock" } ); SECTION( "resumed_craft" ) { - turns_taken = actually_test_craft( test_recipe, tools, INT_MAX ); + turns_taken = resume_craft(); CHECK( turns_taken == expected_turns_taken - 2 ); - verify_inventory( { "crude_picklock" }, { "scrap" } ); + verify_inventory( { "crude_picklock" }, { "craft" } ); } } }