diff --git a/data/json/item_actions.json b/data/json/item_actions.json index 9db1eb413694..d51602a8af37 100644 --- a/data/json/item_actions.json +++ b/data/json/item_actions.json @@ -369,6 +369,11 @@ "id": "COIN_FLIP", "name": { "str": "Flip" } }, + { + "type": "item_action", + "id": "BINDER_ADD_RECIPE", + "name": { "str": "Copy a recipe" } + }, { "type": "item_action", "id": "COMBATSAW_OFF", diff --git a/data/json/itemgroups/SUS/office.json b/data/json/itemgroups/SUS/office.json index 3cfb1e23cad7..56f15b4d97a3 100644 --- a/data/json/itemgroups/SUS/office.json +++ b/data/json/itemgroups/SUS/office.json @@ -15,6 +15,7 @@ { "item": "file", "count": [ 1, 8 ], "prob": 50 }, { "item": "paper", "count": [ 2, 9 ], "prob": 60 }, { "item": "pen", "count": [ 1, 10 ], "prob": 95 }, + { "item": "book_binder", "prob": 90 }, { "item": "stapler", "prob": 60 }, { "item": "scissors", "prob": 35 }, { "item": "phonebook", "prob": 10 }, @@ -32,6 +33,7 @@ "entries": [ { "item": "file", "count": [ 1, 15 ], "prob": 95 }, { "item": "paper", "count": [ 2, 30 ], "prob": 80 }, + { "item": "book_binder", "prob": 90 }, { "item": "mobile_memory_card_used", "count": [ 1, 4 ], "prob": 15 }, { "item": "mobile_memory_card_encrypted", "count": [ 1, 2 ], "prob": 15 } ] diff --git a/data/json/items/ammo_types.json b/data/json/items/ammo_types.json index 62583254eb9a..fb9c0d079077 100644 --- a/data/json/items/ammo_types.json +++ b/data/json/items/ammo_types.json @@ -599,6 +599,12 @@ "name": "compressed air", "default": "nitrox" }, + { + "type": "ammunition_type", + "id": "paper", + "name": "paper", + "default": "paper" + }, { "id": "tinder", "name": "tinder", diff --git a/data/json/items/comestibles/other.json b/data/json/items/comestibles/other.json index 8cb169b11cf3..bb141ee7e99c 100644 --- a/data/json/items/comestibles/other.json +++ b/data/json/items/comestibles/other.json @@ -233,14 +233,14 @@ }, { "type": "COMESTIBLE", - "id": "paper", - "name": "paper", + "id": "napkin", + "name": { "str": "napkin" }, "category": "spare_parts", "weight": "3 g", "color": "white", "comestible_type": "FOOD", "symbol": "`", - "description": "A piece of paper. Can be used for fires.", + "description": "A napkin. Can be used for fires.", "price": 0, "price_postapoc": 0, "material": "paper", diff --git a/data/json/items/generic.json b/data/json/items/generic.json index ca371783a47e..b92b31f60683 100644 --- a/data/json/items/generic.json +++ b/data/json/items/generic.json @@ -1174,6 +1174,43 @@ "to_hit": -3, "flags": [ "TRADER_AVOID", "NO_REPAIR" ] }, + { + "type": "AMMO", + "id": "paper", + "name": { "str": "paper" }, + "category": "spare_parts", + "weight": "3 g", + "color": "white", + "symbol": "`", + "description": "A piece of paper. Can be used for fires.", + "price": 0, + "price_postapoc": 0, + "material": [ "paper" ], + "volume": "6 ml", + "ammo_type": "paper" + }, + { + "id": "book_binder", + "type": "TOOL", + "category": "books", + "use_action": "BINDER_ADD_RECIPE", + "symbol": "/", + "looks_like": "story_book", + "color": "light_gray", + "name": { "str": "book binder" }, + "description": "A book binder. With a pen and some paper you could copy some recipes from books.", + "weight": "200 g", + "volume": "640 ml", + "material": [ "plastic" ], + "bashing": 1, + "to_hit": -1, + "price": 500, + "ammo": "paper", + "initial_charges": 1, + "max_charges": 500, + "price_postapoc": 50, + "flags": [ "TRADER_AVOID", "NO_UNLOAD", "NO_RELOAD" ] + }, { "type": "GENERIC", "id": "broken_dispatch_military", diff --git a/data/json/items/tool/stationary.json b/data/json/items/tool/stationary.json index 09c96fdcedd1..2a5283b2bc40 100644 --- a/data/json/items/tool/stationary.json +++ b/data/json/items/tool/stationary.json @@ -15,7 +15,7 @@ }, { "id": "pen", - "type": "GENERIC", + "type": "TOOL", "category": "tools", "name": { "str": "pen" }, "description": "A plastic ball point pen.", @@ -25,7 +25,11 @@ "price_postapoc": 10, "material": "plastic", "symbol": ";", - "color": "light_gray" + "color": "light_gray", + "initial_charges": 200, + "max_charges": 200, + "charges_per_use": 1, + "flags": [ "WRITE_MESSAGE" ] }, { "id": "scissors", diff --git a/data/json/player_activities.json b/data/json/player_activities.json index 86ea2913ca07..7518e9c72f69 100644 --- a/data/json/player_activities.json +++ b/data/json/player_activities.json @@ -750,6 +750,13 @@ "based_on": "time", "no_resume": true }, + { + "id": "ACT_BINDER_COPY_RECIPE", + "type": "activity_type", + "verb": "writing", + "rooted": true, + "based_on": "speed" + }, { "id": "ACT_MIGRATION_CANCEL", "type": "activity_type", diff --git a/data/json/recipes/other/materials.json b/data/json/recipes/other/materials.json index f2e8b235a8d8..6beb2588d83d 100644 --- a/data/json/recipes/other/materials.json +++ b/data/json/recipes/other/materials.json @@ -527,6 +527,7 @@ "subcategory": "CSC_OTHER_MATERIALS", "skill_used": "fabrication", "skills_required": [ "survival", 4 ], + "charges": 50, "difficulty": 5, "time": "2 h", "autolearn": true, diff --git a/data/json/recipes/other/tools.json b/data/json/recipes/other/tools.json index 74f0e1a0e70f..7f03412871b8 100644 --- a/data/json/recipes/other/tools.json +++ b/data/json/recipes/other/tools.json @@ -989,6 +989,17 @@ "autolearn": true, "using": [ [ "blacksmithing_standard", 4 ], [ "steel_standard", 1 ] ] }, + { + "type": "recipe", + "result": "book_binder", + "category": "CC_OTHER", + "subcategory": "CSC_OTHER_OTHER", + "skill_used": "fabrication", + "difficulty": 2, + "time": "45 m", + "autolearn": true, + "components": [ [ [ "duct_tape", 20 ] ], [ [ "plastic_chunk", 2 ] ], [ [ "string_6", 2 ] ] ] + }, { "type": "recipe", "result": "teapot", diff --git a/data/json/tool_qualities.json b/data/json/tool_qualities.json index f5e724df8cee..f7fa504258b7 100644 --- a/data/json/tool_qualities.json +++ b/data/json/tool_qualities.json @@ -161,6 +161,11 @@ "id": "PRY", "name": { "str": "prying" } }, + { + "type": "tool_quality", + "id": "WRITE", + "name": { "str": "writing" } + }, { "type": "tool_quality", "id": "LIFT", diff --git a/doc/JSON_FLAGS.md b/doc/JSON_FLAGS.md index 9067af20c1a7..f85ab56cb1b0 100644 --- a/doc/JSON_FLAGS.md +++ b/doc/JSON_FLAGS.md @@ -163,6 +163,7 @@ These are handled through `ammo_types.json`. You can tag a weapon with these to - ```thrown``` Thrown - ```unfinished_char``` Semi-charred fuel - ```water``` Water +- ```paper``` Paper ### Effects @@ -403,7 +404,7 @@ Some armor flags, such as `WATCH` and `ALARMCLOCK` are compatible with other ite - ```WASH_HARD_ITEMS``` Wash hard items with FILTHY flag. - ```WASH_SOFT_ITEMS``` Wash soft items with FILTHY flag. - ```WATER_PURIFIER``` Purify water. - +- ```BINDER_ADD_RECIPE``` Add recipe to a book binder. ## Comestibles diff --git a/src/activity_actor.cpp b/src/activity_actor.cpp index e0f7d65e4e8a..942438048fd1 100644 --- a/src/activity_actor.cpp +++ b/src/activity_actor.cpp @@ -13,6 +13,7 @@ #include "avatar.h" #include "calendar.h" #include "character.h" +#include "character_functions.h" #include "crafting.h" #include "debug.h" #include "enums.h" @@ -50,6 +51,7 @@ static const itype_id itype_bone_human( "bone_human" ); static const itype_id itype_electrohack( "electrohack" ); +static const itype_id itype_paper( "paper" ); static const skill_id skill_computer( "computer" ); @@ -60,6 +62,7 @@ static const mtype_id mon_skeleton( "mon_skeleton" ); static const mtype_id mon_zombie_crawler( "mon_zombie_crawler" ); static const std::string flag_RELOAD_AND_SHOOT( "RELOAD_AND_SHOOT" ); +static const std::string flag_WRITE_MESSAGE( "WRITE_MESSAGE" ); static const std::string has_thievery_witness( "has_thievery_witness" ); @@ -879,6 +882,74 @@ void hacking_activity_actor::finish( player_activity &act, Character &who ) act.set_to_null(); } +void bookbinder_copy_activity_actor::start( player_activity &act, Character & ) +{ + pages = 1 + rec_id->difficulty / 2; + act.moves_total = to_moves( pages * 1_minutes ); + act.moves_left = to_moves( pages * 1_minutes ); +} + +void bookbinder_copy_activity_actor::do_turn( player_activity &, Character &p ) +{ + if( character_funcs::fine_detail_vision_mod( p ) > 4.0f ) { + p.cancel_activity(); + p.add_msg_if_player( m_info, _( "It's too dark to write!" ) ); + return; + } +} + +void bookbinder_copy_activity_actor::finish( player_activity &act, Character &p ) +{ + const bool rec_added = book_binder->eipc_recipe_add( rec_id ); + if( rec_added ) { + p.add_msg_if_player( m_good, _( "You copy the recipe for %1$s into your recipe book." ), + rec_id->result_name() ); + + p.use_charges( itype_paper, pages ); + book_binder.get_item()->charges += pages; + + const std::vector writing_tools_filter = + p.crafting_inventory().items_with( [&]( const item & it ) { + return it.has_flag( flag_WRITE_MESSAGE ) && it.ammo_remaining() >= it.ammo_required(); + } ); + + std::vector writing_tools; + writing_tools.reserve( writing_tools_filter.size() ); + for( const item *tool : writing_tools_filter ) { + writing_tools.emplace_back( tool_comp( tool->typeId(), 1 ) ); + } + + player *player = p.as_player(); + + player->consume_tools( writing_tools, pages ); + } else { + debugmsg( "Recipe book already has '%s' recipe when it should not.", rec_id.str() ); + } + + act.set_to_null(); +} + +void bookbinder_copy_activity_actor::serialize( JsonOut &jsout ) const +{ + jsout.start_object(); + jsout.member( "book_binder", book_binder ); + jsout.member( "rec_id", rec_id ); + jsout.member( "pages", pages ); + jsout.end_object(); +} + +std::unique_ptr bookbinder_copy_activity_actor::deserialize( JsonIn &jsin ) +{ + bookbinder_copy_activity_actor actor; + + JsonObject jsobj = jsin.get_object(); + jsobj.read( "book_binder", actor.book_binder ); + jsobj.read( "rec_id", actor.rec_id ); + jsobj.read( "pages", actor.pages ); + + return actor.clone(); +} + void hacking_activity_actor::serialize( JsonOut &jsout ) const { jsout.start_object(); @@ -1270,6 +1341,7 @@ const std::unordered_map( * )( Json deserialize_functions = { { activity_id( "ACT_AIM" ), &aim_activity_actor::deserialize }, { activity_id( "ACT_AUTODRIVE" ), &autodrive_activity_actor::deserialize }, + { activity_id( "ACT_BINDER_COPY_RECIPE" ), &bookbinder_copy_activity_actor::deserialize }, { activity_id( "ACT_DIG" ), &dig_activity_actor::deserialize }, { activity_id( "ACT_DIG_CHANNEL" ), &dig_channel_activity_actor::deserialize }, { activity_id( "ACT_DROP" ), &drop_activity_actor::deserialize }, diff --git a/src/activity_actor_definitions.h b/src/activity_actor_definitions.h index dc486221e5e4..5484a81f9ff9 100644 --- a/src/activity_actor_definitions.h +++ b/src/activity_actor_definitions.h @@ -109,6 +109,43 @@ class autodrive_activity_actor : public activity_actor static std::unique_ptr deserialize( JsonIn &jsin ); }; +class bookbinder_copy_activity_actor : public activity_actor +{ + private: + item_location book_binder; + recipe_id rec_id; + int pages = 0; + + public: + + bookbinder_copy_activity_actor() = default; + bookbinder_copy_activity_actor( + const item_location &book_binder, + const recipe_id &rec_id + ) : book_binder( book_binder ), rec_id( rec_id ) {}; + + activity_id get_type() const override { + return activity_id( "ACT_BINDER_COPY_RECIPE" ); + } + + bool can_resume_with_internal( const activity_actor &other, const Character & ) const override { + const bookbinder_copy_activity_actor &act = static_cast + ( other ); + return rec_id == act.rec_id && book_binder == act.book_binder; + } + + void start( player_activity &act, Character & ) override; + void do_turn( player_activity &, Character &p ) override; + void finish( player_activity &act, Character &p ) override; + + std::unique_ptr clone() const override { + return std::make_unique( *this ); + } + + void serialize( JsonOut &jsout ) const override; + static std::unique_ptr deserialize( JsonIn &jsin ); +}; + class dig_activity_actor : public activity_actor { private: diff --git a/src/character.cpp b/src/character.cpp index 0c8c71393c57..82848a0a339f 100644 --- a/src/character.cpp +++ b/src/character.cpp @@ -10598,4 +10598,4 @@ bool has_psy_protection( const Character &c, int partial_chance ) { return c.has_artifact_with( AEP_PSYSHIELD ) || ( c.worn_with_flag( "PSYSHIELD_PARTIAL" ) && one_in( partial_chance ) ); -} \ No newline at end of file +} diff --git a/src/item.cpp b/src/item.cpp index 9a309515ca8b..65ae9a8c45ce 100644 --- a/src/item.cpp +++ b/src/item.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -6972,17 +6973,19 @@ void item::mark_chapter_as_read( const Character &ch ) set_var( var, remain ); } -std::vector> item::get_available_recipes( const player &u ) const +std::vector> item::get_available_recipes( const player &u, + bool bypass_skill_requirement ) const { std::vector> recipe_entries; if( is_book() ) { for( const islot_book::recipe_with_description_t &elem : type->book->recipes ) { - if( u.get_skill_level( elem.recipe->skill_used ) >= elem.skill_level ) { + if( u.get_skill_level( elem.recipe->skill_used ) >= elem.skill_level || + bypass_skill_requirement ) { recipe_entries.push_back( std::make_pair( elem.recipe, elem.skill_level ) ); } } } else if( has_var( "EIPC_RECIPES" ) ) { - // See einkpc_download_memory_card() in iuse.cpp where this is set. + // See eipc_recipe_add() in item.cpp where this is set. const std::string recipes = get_var( "EIPC_RECIPES" ); // Capture the index one past the delimiter, i.e. start of target string. size_t first_string_index = recipes.find_first_of( ',' ) + 1; @@ -6994,7 +6997,8 @@ std::vector> item::get_available_recipes( const p std::string new_recipe = recipes.substr( first_string_index, next_string_index - first_string_index ); const recipe *r = &recipe_id( new_recipe ).obj(); - if( u.get_skill_level( r->skill_used ) >= r->difficulty ) { + + if( u.get_skill_level( r->skill_used ) >= r->difficulty || bypass_skill_requirement ) { recipe_entries.push_back( std::make_pair( r, r->difficulty ) ); } first_string_index = next_string_index + 1; @@ -7003,6 +7007,22 @@ std::vector> item::get_available_recipes( const p return recipe_entries; } +bool item::eipc_recipe_add( const recipe_id &recipe_id ) +{ + bool recipe_success = false; + + const std::string old_recipes = this->get_var( "EIPC_RECIPES" ); + if( old_recipes.empty() ) { + recipe_success = true; + this->set_var( "EIPC_RECIPES", "," + recipe_id.str() + "," ); + } else if( old_recipes.find( "," + recipe_id.str() + "," ) == std::string::npos ) { + recipe_success = true; + this->set_var( "EIPC_RECIPES", old_recipes + recipe_id.str() + "," ); + } + + return recipe_success; +} + const material_type &item::get_random_material() const { return random_entry( made_of(), material_id::NULL_ID() ).obj(); @@ -7246,6 +7266,7 @@ units::energy item::energy_remaining() const int item::ammo_remaining() const { + // Magazine in the item const item *mag = magazine_current(); if( mag ) { return mag->ammo_remaining(); diff --git a/src/item.h b/src/item.h index d87bc0be29be..a06e6ca115db 100644 --- a/src/item.h +++ b/src/item.h @@ -1668,9 +1668,17 @@ class item : public visitable /** * Enumerates recipes available from this book and the skill level required to use them. */ - std::vector> get_available_recipes( const player &u ) const; + std::vector> get_available_recipes( const player &u, + bool bypass_skill_requirement = false ) const; /*@}*/ + /** + * Add a recipe to the EIPC_RECIPES variable. + * + * @return true if the recipe was added, false if it is a duplicate + */ + bool eipc_recipe_add( const recipe_id &recipe_id ); + /** * @name Martial art techniques * diff --git a/src/item_factory.cpp b/src/item_factory.cpp index 4cf42f174ddc..abd0883a149b 100644 --- a/src/item_factory.cpp +++ b/src/item_factory.cpp @@ -991,6 +991,7 @@ void Item_factory::init() add_iuse( "RADIO_MOD", &iuse::radio_mod ); add_iuse( "RADIO_OFF", &iuse::radio_off ); add_iuse( "RADIO_ON", &iuse::radio_on ); + add_iuse( "BINDER_ADD_RECIPE", &iuse::binder_add_recipe ); add_iuse( "REMOTEVEH", &iuse::remoteveh ); add_iuse( "REMOVE_ALL_MODS", &iuse::remove_all_mods ); add_iuse( "REPORT_GRID_CHARGE", &iuse::report_grid_charge ); diff --git a/src/iuse.cpp b/src/iuse.cpp index 9945ca92fe2c..4edc79107f8a 100644 --- a/src/iuse.cpp +++ b/src/iuse.cpp @@ -266,6 +266,7 @@ static const itype_id itype_radio( "radio" ); static const itype_id itype_radio_car( "radio_car" ); static const itype_id itype_radio_car_on( "radio_car_on" ); static const itype_id itype_radio_on( "radio_on" ); +static const itype_id itype_paper( "paper" ); static const itype_id itype_rebreather_on( "rebreather_on" ); static const itype_id itype_rebreather_xl_on( "rebreather_xl_on" ); static const itype_id itype_rmi2_corpse( "rmi2_corpse" ); @@ -355,6 +356,7 @@ static const std::string flag_FIX_FARSIGHT( "FIX_FARSIGHT" ); static const std::string flag_HEATS_FOOD( "HEATS_FOOD" ); static const std::string flag_PLANT( "PLANT" ); static const std::string flag_PLOWABLE( "PLOWABLE" ); +static const std::string flag_WRITE_MESSAGE( "WRITE_MESSAGE" ); // how many characters per turn of radio static constexpr int RADIO_PER_TURN = 25; @@ -6259,26 +6261,16 @@ static bool einkpc_download_memory_card( player &p, item &eink, item &mc ) if( !candidates.empty() ) { const recipe *r = random_entry( candidates ); - const recipe_id &rident = r->ident(); - const auto old_recipes = eink.get_var( "EIPC_RECIPES" ); - if( old_recipes.empty() ) { - something_downloaded = true; - eink.set_var( "EIPC_RECIPES", "," + rident.str() + "," ); + bool rec_added = eink.eipc_recipe_add( r->ident() ); + if( rec_added ) { + something_downloaded = true; p.add_msg_if_player( m_good, _( "You download a recipe for %s into the tablet's memory." ), r->result_name() ); } else { - if( old_recipes.find( "," + rident.str() + "," ) == std::string::npos ) { - something_downloaded = true; - eink.set_var( "EIPC_RECIPES", old_recipes + rident.str() + "," ); - - p.add_msg_if_player( m_good, _( "You download a recipe for %s into the tablet's memory." ), - r->result_name() ); - } else { - p.add_msg_if_player( m_good, _( "Your tablet already has a recipe for %s." ), - r->result_name() ); - } + p.add_msg_if_player( m_good, _( "Your tablet already has a recipe for %s." ), + r->result_name() ); } } } @@ -9757,3 +9749,211 @@ int use_function::call( player &p, item &it, bool active, const tripoint &pos ) { return actor->use( p, it, active, pos ); } + +/** + * Remove a recipe from the EIPC_RECIPES variable. + * + * @return true if the recipe was deleted + */ +bool static eipc_recipe_remove( item &it, const recipe_id &recipe_id ) +{ + bool recipe_success = false; + + std::string current_recipes = it.get_var( "EIPC_RECIPES" ); + if( current_recipes.empty() ) { + return false; + } else if( current_recipes.find( "," + recipe_id.str() + "," ) != std::string::npos ) { + recipe_success = true; + current_recipes.replace( current_recipes.find( recipe_id.str() ), recipe_id.str().length() + 1, + "" ); + it.set_var( "EIPC_RECIPES", current_recipes ); + } + + return recipe_success; +} + +int iuse::binder_add_recipe( player *p, item *binder, bool, const tripoint & ) +{ + if( p->is_underwater() ) { + p->add_msg_if_player( m_info, _( "You rethink trying to write underwater." ) ); + return 0; + } + + uilist amenu; + amenu.text = _( "Choose menu option:" ); + + if( !binder->get_var( "EIPC_RECIPES" ).empty() ) { + amenu.addentry( 0, true, 'r', _( "View recipes" ) ); + } + amenu.addentry( 1, true, 'w', _( "Copy a recipe from a book" ) ); + + amenu.query(); + if( amenu.ret < 0 ) { + return 0; + } else if( amenu.ret == 0 ) { + uilist rmenu; + rmenu.text = _( "List recipes:" ); + + std::vector candidate_recipes; + std::istringstream f( binder->get_var( "EIPC_RECIPES" ) ); + std::string s; + int k = 0; + while( getline( f, s, ',' ) ) { + + if( s.empty() ) { + continue; + } + + candidate_recipes.emplace_back( s ); + + const recipe &rec = *candidate_recipes.back(); + const int pages = 1 + rec.difficulty / 2; + if( rec ) { + rmenu.addentry_col( k++, true, -1, rec.result_name(), + string_format( vgettext( "%1$d page", "%1$d pages", pages ), pages ) ); + } + } + + rmenu.query(); + + return 0; + } + + const inventory crafting_inv = p->crafting_inventory(); + const std::vector writing_tools = crafting_inv.items_with( [&]( const item & it ) { + return it.has_flag( flag_WRITE_MESSAGE ) && it.ammo_remaining() >= it.ammo_required(); + } ); + + if( writing_tools.empty() ) { + p->add_msg_if_player( m_info, _( "You do not have anything to write with." ) ); + return 0; + } + const std::vector helpers = p->get_crafting_helpers(); + + // get recipes no matter the skill requirement + recipe_subset available_recipes; + for( const auto &stack : crafting_inv.const_slice() ) { + const item &candidate = stack->front(); + + for( std::pair recipe_entry : + candidate.get_available_recipes( *p, true ) ) { + available_recipes.include( recipe_entry.first, recipe_entry.second ); + } + } + + + std::vector not_learnt_recipes; + std::string old_recipes = binder->get_var( "EIPC_RECIPES" ); + recipe_subset learnt_recipes = p->get_learned_recipes(); + // automatically remove already learnt recipes from the book binder, and free book space + int total_pages_removed = 0; + for( const recipe *rec : available_recipes.get_recipes() ) { + if( p->knows_recipe( rec ) ) { + // remove the recipe + if( eipc_recipe_remove( *binder, rec->ident() ) ) { + // remove the pages from the book + int pages_removed = 1 + rec->difficulty / 2; + binder->charges -= pages_removed; + total_pages_removed += pages_removed; + } + } + } + + if( total_pages_removed != 0 ) { + p->add_msg_if_player( m_info, _ + ( string_format( "You already know some recipes. You remove %d pages from the book binder.", + total_pages_removed ) ) ); + } + + // only keep not learnt recipes and those that are not already in the book + old_recipes = binder->get_var( "EIPC_RECIPES" ); + for( const recipe *rec : available_recipes.get_recipes() ) { + bool add_recipe = true; + for( const recipe *rec_l : learnt_recipes.get_recipes() ) { + // if it's an available recipe, and it's learned, change the flag so it's not added + if( rec == rec_l ) { + add_recipe = false; + break; + } + } + // already in book binder? + if( old_recipes.find( "," + rec->ident().str() + "," ) != std::string::npos ) { + add_recipe = false; + } + + if( add_recipe ) { + not_learnt_recipes.emplace_back( rec ); + } + } + + if( not_learnt_recipes.empty() ) { + p->add_msg_if_player( m_info, _( "You do not have any recipes you can copy." ) ); + return 0; + } + + // if player doesn't have at least 1 paper on him, add an early warning message + const int papers_on_player = p->charges_of( itype_paper ); + if( papers_on_player <= 0 ) { + p->add_msg_if_player( m_info, _( "You do not have paper to copy a recipe." ) ); + return 0; + } + + + const int charges_left = binder->type->maximum_charges() - binder->charges; + if( charges_left == 0 ) { + p->add_msg_if_player( m_info, _( "Your book binder is full." ) ); + return 0; + } + + if( character_funcs::fine_detail_vision_mod( *p ) > 4.0f ) { + p->add_msg_if_player( m_info, _( "It's too dark to write!" ) ); + return 0; + } + + uilist menu; + menu.text = _( "Choose recipe to copy" ); + + for( const recipe *rec : not_learnt_recipes ) { + + const int pages = 1 + rec->difficulty / 2; + // greyed out row if there's not enough space in the book, or if there's not enough paper on the player to write the recipe + menu.addentry_col( -1, ( charges_left >= pages ) && ( papers_on_player >= pages ), ' ', + rec->result_name(), + string_format( vgettext( "%1$d page", "%1$d pages", pages ), pages ) ); + } + + menu.query(); + if( menu.ret < 0 ) { + return 0; + } + + const int pages = 1 + not_learnt_recipes[menu.ret]->difficulty / 2; + bool has_enough_charges = false; + + // one charge per page + int max_charges = 0; + for( const item *it : writing_tools ) { + if( it->ammo_required() == 0 ) { + has_enough_charges = true; + break; + } + + max_charges += it->ammo_remaining() / it->ammo_required(); + if( max_charges >= pages ) { + has_enough_charges = true; + break; + } + } + + if( !has_enough_charges ) { + p->add_msg_if_player( m_info, _( "Your writing tool does not have enough charges." ) ); + return 0; + } + + p->assign_activity( player_activity( + bookbinder_copy_activity_actor( + item_location( *p, binder ), + not_learnt_recipes[menu.ret]->ident() ) ) ); + + return 0; +} diff --git a/src/iuse.h b/src/iuse.h index 76800c713592..e91ac520f0c1 100644 --- a/src/iuse.h +++ b/src/iuse.h @@ -172,6 +172,7 @@ int toolmod_attach( player *, item *, bool, const tripoint & ); int rm13armor_off( player *, item *, bool, const tripoint & ); int rm13armor_on( player *, item *, bool, const tripoint & ); int unpack_item( player *, item *, bool, const tripoint & ); +int binder_add_recipe( player *, item *, bool, const tripoint & ); int pack_cbm( player *p, item *it, bool, const tripoint & ); int pack_item( player *, item *, bool, const tripoint & ); int radglove( player *, item *, bool, const tripoint & ); diff --git a/src/recipe_dictionary.cpp b/src/recipe_dictionary.cpp index 6ea2d58a0d53..93ae0b684da5 100644 --- a/src/recipe_dictionary.cpp +++ b/src/recipe_dictionary.cpp @@ -245,6 +245,12 @@ std::vector recipe_subset::search_result( const itype_id &item ) return res; } +std::vector recipe_subset::get_recipes() +{ + std::vector ret( recipes.begin(), recipes.end() ); + return ret; +} + bool recipe_subset::empty_category( const std::string &cat, const std::string &subcat ) const { if( subcat == "CSC_*_FAVORITE" ) { diff --git a/src/recipe_dictionary.h b/src/recipe_dictionary.h index 8d1011fcd825..9d1b3476ffb6 100644 --- a/src/recipe_dictionary.h +++ b/src/recipe_dictionary.h @@ -177,6 +177,8 @@ class recipe_subset return recipes.end(); } + std::vector get_recipes(); + private: std::set recipes; std::map difficulties;