From 8021fe4f9681e2dd2c9224813c8d3b34177af5ae Mon Sep 17 00:00:00 2001 From: Mark Langsdorf Date: Mon, 3 Jun 2019 20:14:34 -0500 Subject: [PATCH] basecamps: add support for blueprint_excludes add a new blueprint keyword "blueprint_excludes" which can be used to create mutually exclusive branches in an camp upgrade path. the list of completed blueprint_provides is compared to an upgrade's blueprint_excludes. If there is a match, the recipe isn't available. There is a second list of in_progress provides, which is used to prevent people from building two mutually exclusive upgrades at the same time, while also preventing them from building upgrade B that requires upgrade A before upgrade A is completed. --- .../basecamps/recipe_primitive_field.json | 21 ++++--- doc/BASECAMP.md | 7 ++- src/basecamp.cpp | 57 +++++++++++++++++-- src/basecamp.h | 7 ++- src/faction_camp.cpp | 17 +++--- src/recipe.cpp | 13 +++++ src/recipe.h | 2 + src/savegame_json.cpp | 16 ++++++ 8 files changed, 117 insertions(+), 23 deletions(-) diff --git a/data/json/recipes/basecamps/recipe_primitive_field.json b/data/json/recipes/basecamps/recipe_primitive_field.json index 5a8b8f03f6b6b..32110acbe6c96 100644 --- a/data/json/recipes/basecamps/recipe_primitive_field.json +++ b/data/json/recipes/basecamps/recipe_primitive_field.json @@ -10,7 +10,8 @@ "never_learn": true, "time": "60 m", "construction_blueprint": "faction_base_field_camp_0", - "blueprint_name": "basic survey" + "blueprint_name": "basic survey", + "blueprint_requires": [ { "id": "not_an_upgrade" } ] }, { "type": "recipe", @@ -462,7 +463,8 @@ "never_learn": true, "construction_blueprint": "faction_base_field_camp_farm_0", "blueprint_name": "Farm survey", - "time": "180 m" + "time": "180 m", + "blueprint_requires": [ { "id": "not_an_upgrade" } ] }, { "type": "recipe", @@ -544,7 +546,8 @@ "construction_blueprint": "faction_base_field_camp_garage_0", "blueprint_name": "Garage survey", "never_learn": true, - "time": "180 m" + "time": "180 m", + "blueprint_requires": [ { "id": "not_an_upgrade" } ] }, { "type": "recipe", @@ -685,7 +688,8 @@ "never_learn": true, "construction_blueprint": "faction_base_field_camp_kitchen_0", "blueprint_name": "Kitchen survey", - "time": "180 m" + "time": "180 m", + "blueprint_requires": [ { "id": "not_an_upgrade" } ] }, { "type": "recipe", @@ -890,7 +894,8 @@ "never_learn": true, "time": "510 m", "qualities": [ [ { "id": "DIG", "level": 1 } ] ], - "components": [ [ [ "stick", 3 ] ] ] + "components": [ [ [ "stick", 3 ] ] ], + "blueprint_requires": [ { "id": "not_an_upgrade" } ] }, { "type": "recipe", @@ -904,7 +909,8 @@ "never_learn": true, "time": "1530 m", "qualities": [ [ { "id": "DIG", "level": 2 } ] ], - "components": [ [ [ "pointy_stick", 68 ] ] ] + "components": [ [ [ "pointy_stick", 68 ] ] ], + "blueprint_requires": [ { "id": "not_an_upgrade" } ] }, { "type": "recipe", @@ -917,7 +923,8 @@ "never_learn": true, "construction_blueprint": "faction_base_field_camp_blacksmith_0", "blueprint_name": "Blacksmithy survey", - "time": "180 m" + "time": "180 m", + "blueprint_requires": [ { "id": "not_an_upgrade" } ] }, { "type": "recipe", diff --git a/doc/BASECAMP.md b/doc/BASECAMP.md index a22834779a5f8..a228ef00c1bbb 100644 --- a/doc/BASECAMP.md +++ b/doc/BASECAMP.md @@ -18,10 +18,13 @@ New field | Description `"construction_name"` | string, a short description/name of the upgrade mission that is displayed in the base camp mission selector. The recipe's `"description"` field has the extended description that explains why a player might want to have an NPC perform this upgrade mission. `"blueprint_provides"` | array of objects, with each object having an `"id"` string and an optional `"amount"` integer. These are the camp features that are available when the upgrade mission is complete. Every upgrade mission provides its recipe `"result"` with an amount of 1 automatically and that string does not need to be listed in `"blueprint_provides"`. `"blueprint_requires"` | array of objects, with each object having an `"id"` string and an optional `"amount"` integer. These are the camp features that are required before the upgrade mission can be attempted. +`"blueprint_excludes"` | array of objects, with each object having an `"id"` string and an optional `"amount"` integer. These are the camp features that prevent the upgrade mission from being attempted if they exist. `"blueprint_resources"` | array of `"itype_id"`s. Items with those ids will be added to the camp inventory after the upgrade mission is completed and can be used for crafting or additional upgrade missions. -### blueprint requires and provides -blueprint requires and blueprint provides are abstract concepts or flags that an upgrade mission requires to start, or that are provided by a previous upgrade mission to satisfy the blueprint requirements of a current upgrade mission. Each one has an `"id"` and an `"amount"`. Multiple requires or provides with the same `"id"` sum their `"amount"` if they're on the same basecamp expansion. +### blueprint requires, provides, and excludes +blueprint requires, blueprint provides, and blueprint exlcudes are abstract concepts or flags that an upgrade mission requires to start, or that are provided by a previous upgrade mission to satisfy the blueprint requirements of a current upgrade mission, or that prevent an upgrade mission from being available. Each one has an `"id"` and an `"amount"`. Multiple requires, provides, or excludes with the same `"id"` sum their `"amount"` if they're on the same basecamp expansion. + +Every upgrade mission has its recipe `"result"` as a blueprint_provides and a blueprint_excludes, so upgrade missions will automatically prevent themselves from being repatable. These are arbitrary strings and can be used to control the branching of the upgrade paths. However, some strings have meaning within the basecamp code: diff --git a/src/basecamp.cpp b/src/basecamp.cpp index 7c0119771adfc..5a9fc273677af 100644 --- a/src/basecamp.cpp +++ b/src/basecamp.cpp @@ -124,6 +124,7 @@ void basecamp::add_expansion( const std::string &terrain, const tripoint &new_po const std::string dir = talk_function::om_simple_dir( omt_pos, new_pos ); expansions[ dir ] = parse_expansion( terrain, new_pos ); + update_provides( terrain, expansions[ dir ] ); directions.push_back( dir ); } @@ -244,18 +245,42 @@ const std::vector basecamp::available_upgrades( const std::str for( int number = 1; number < base_camps::max_upgrade_by_type( e_data.type ); number++ ) { const std::string &bldg = base_camps::faction_encode_abs( e_data, number ); const recipe &recp = recipe_id( bldg ).obj(); - bool should_display = false; + // skip buildings that are completed + if( e_data.provides.find( bldg ) != e_data.provides.end() ) { + continue; + } + // skip building that have unmet requirements + size_t needed_requires = recp.blueprint_requires().size(); + size_t met_requires = 0; for( const auto &bp_require : recp.blueprint_requires() ) { - if( e_data.provides.find( bldg ) != e_data.provides.end() ) { - break; - } if( e_data.provides.find( bp_require.first ) == e_data.provides.end() ) { break; } if( e_data.provides[bp_require.first] < bp_require.second ) { break; } - should_display = true; + met_requires += 1; + } + if( met_requires < needed_requires ) { + continue; + } + bool should_display = true; + bool in_progress = false; + for( const auto &bp_exclude : recp.blueprint_excludes() ) { + // skip buildings that are excluded by previous builds + if( e_data.provides.find( bp_exclude.first ) != e_data.provides.end() ) { + if( e_data.provides[bp_exclude.first] >= bp_exclude.second ) { + should_display = false; + break; + } + } + // track buildings that are currently being built + if( e_data.in_progress.find( bp_exclude.first ) != e_data.in_progress.end() ) { + if( e_data.in_progress[bp_exclude.first] >= bp_exclude.second ) { + in_progress = true; + break; + } + } } if( !should_display ) { continue; @@ -265,6 +290,7 @@ const std::vector basecamp::available_upgrades( const std::str data.name = recp.blueprint_name(); const auto &reqs = recp.requirements(); data.avail = reqs.can_make_with_inventory( _inv, recp.get_component_filter(), 1 ); + data.in_progress = in_progress; ret_data.emplace_back( data ); } } @@ -340,6 +366,27 @@ void basecamp::update_provides( const std::string &bldg, expansion_data &e_data } } + +void basecamp::update_in_progress( const std::string &bldg, const std::string &dir ) +{ + if( !recipe_id( bldg ).is_valid() ) { + return; + } + auto e = expansions.find( dir ); + if( e == expansions.end() ) { + return; + } + expansion_data &e_data = e->second; + + const recipe &making = recipe_id( bldg ).obj(); + for( const auto &bp_provides : making.blueprint_provides() ) { + if( e_data.in_progress.find( bp_provides.first ) == e_data.in_progress.end() ) { + e_data.in_progress[bp_provides.first] = 0; + } + e_data.in_progress[bp_provides.first] += bp_provides.second; + } +} + void basecamp::reset_camp_resources( bool by_radio ) { reset_camp_workers(); diff --git a/src/basecamp.h b/src/basecamp.h index cc4929da36d7c..6a5b89f5f71ff 100644 --- a/src/basecamp.h +++ b/src/basecamp.h @@ -28,6 +28,7 @@ class mission_data; struct expansion_data { std::string type; std::map provides; + std::map in_progress; tripoint pos; // legacy camp level, replaced by provides map and set to -1 int cur_level; @@ -74,6 +75,7 @@ struct basecamp_upgrade { std::string bldg; std::string name; bool avail = false; + bool in_progress = false; }; class basecamp @@ -119,7 +121,7 @@ class basecamp bool has_provides( const std::string &req, const std::string &dir = "all", int level = 0 ) const; void update_resources( const std::string &bldg ); void update_provides( const std::string &bldg, expansion_data &e_data ); - + void update_in_progress( const std::string &bldg, const std::string &dir ); bool can_expand(); /// Returns the name of the building the current building @ref dir upgrades into, @@ -215,7 +217,8 @@ class basecamp bool must_feed, const std::string &desc, bool group, const std::vector &equipment, const std::string &skill_tested, int skill_level ); - void start_upgrade( const std::string &bldg, const std::string &key, bool by_radio ); + void start_upgrade( const std::string &bldg, const std::string &dir, const std::string &key, + bool by_radio ); std::string om_upgrade_description( const std::string &bldg, bool trunc = false ) const; void start_menial_labor(); void start_crafting( const std::string &cur_id, const std::string &cur_dir, diff --git a/src/faction_camp.cpp b/src/faction_camp.cpp index eff94eceb63ea..370285ccfd33c 100644 --- a/src/faction_camp.cpp +++ b/src/faction_camp.cpp @@ -601,11 +601,11 @@ void basecamp::get_available_missions( mission_data &mission_key, bool by_radio gather_bldg = upgrade.bldg; const base_camps::miss_data &miss_info = base_camps::miss_info[ "_faction_upgrade_camp" ]; comp_list npc_list = get_mission_workers( upgrade.bldg + "_faction_upgrade_camp" ); - if( npc_list.empty() ) { + if( npc_list.empty() && !upgrade.in_progress ) { entry = om_upgrade_description( upgrade.bldg ); mission_key.add_start( miss_info.miss_id + upgrade.bldg, miss_info.desc + " " + upgrade.name, "", entry, upgrade.avail ); - } else { + } else if( !npc_list.empty() && upgrade.in_progress ) { entry = miss_info.action; bool avail = update_time_left( entry, npc_list ); mission_key.add_return( miss_info.ret_miss_id + upgrade.bldg, @@ -1190,7 +1190,7 @@ bool basecamp::handle_mission( const std::string &miss_id, const std::string &mi if( miss_id.size() > 12 && miss_id.substr( 0, 12 ) == "Upgrade Camp" ) { const std::string bldg = miss_id.substr( 12 ); - start_upgrade( bldg, bldg + "_faction_upgrade_camp", by_radio ); + start_upgrade( bldg, base_dir, bldg + "_faction_upgrade_camp", by_radio ); } else if( miss_id == "Recover Ally from Upgrading" ) { upgrade_return( base_dir, "_faction_upgrade_camp" ); } else if( miss_id.size() > 27 && miss_id.substr( 0, 27 ) == "Recover Ally from Upgrading" ) { @@ -1313,7 +1313,7 @@ bool basecamp::handle_mission( const std::string &miss_id, const std::string &mi if( miss_id.size() > 19 && miss_id.substr( 0, 19 ) == ( miss_dir + " Expansion Upgrade" ) ) { const std::string bldg = miss_id.substr( 19 ); - start_upgrade( bldg, bldg + "_faction_upgrade_exp_" + miss_dir, by_radio ); + start_upgrade( bldg, dir, bldg + "_faction_upgrade_exp_" + miss_dir, by_radio ); } else if( miss_id == "Recover Ally, " + miss_dir + " Expansion" ) { upgrade_return( dir, "_faction_upgrade_exp_" + miss_dir ); } else { @@ -1406,7 +1406,8 @@ npc_ptr basecamp::start_mission( const std::string &miss_id, time_duration durat return comp; } -void basecamp::start_upgrade( const std::string &bldg, const std::string &key, bool by_radio ) +void basecamp::start_upgrade( const std::string &bldg, const std::string &dir, + const std::string &key, bool by_radio ) { const recipe &making = recipe_id( bldg ).obj(); //Stop upgrade if you don't have materials @@ -1417,9 +1418,11 @@ void basecamp::start_upgrade( const std::string &bldg, const std::string &key, b npc_ptr comp = start_mission( key, work_days, must_feed, _( "begins to upgrade the camp..." ), false, {}, making.skill_used.str(), making.difficulty ); - if( comp != nullptr ) { - consume_components( making, 1, by_radio ); + if( comp == nullptr ) { + return; } + consume_components( making, 1, by_radio ); + update_in_progress( bldg, dir ); } else { popup( _( "You don't have the materials for the upgrade." ) ); } diff --git a/src/recipe.cpp b/src/recipe.cpp index d68069d3efd57..5ef561193f692 100644 --- a/src/recipe.cpp +++ b/src/recipe.cpp @@ -247,6 +247,14 @@ void recipe::load( JsonObject &jo, const std::string &src ) bp_requires.emplace_back( std::make_pair( require.get_string( "id" ), require.get_int( "amount", 1 ) ) ); } + // all blueprints exclude themselves with needing it written in JSON + bp_excludes.emplace_back( std::make_pair( result_, 1 ) ); + bp_array = jo.get_array( "blueprint_excludes" ); + while( bp_array.has_more() ) { + JsonObject exclude = bp_array.next_object(); + bp_excludes.emplace_back( std::make_pair( exclude.get_string( "id" ), + exclude.get_int( "amount", 1 ) ) ); + } } } else if( type == "uncraft" ) { reversible = true; @@ -535,6 +543,11 @@ const std::vector> &recipe::blueprint_requires() co return bp_requires; } +const std::vector> &recipe::blueprint_excludes() const +{ + return bp_excludes; +} + bool recipe::hot_result() const { // Check if the recipe tools make this food item hot upon making it. diff --git a/src/recipe.h b/src/recipe.h index 84065fefa8883..60c9192a39ede 100644 --- a/src/recipe.h +++ b/src/recipe.h @@ -126,6 +126,7 @@ class recipe const std::vector &blueprint_resources() const; const std::vector> &blueprint_provides() const; const std::vector> &blueprint_requires() const; + const std::vector> &blueprint_excludes() const; bool hot_result() const; @@ -174,6 +175,7 @@ class recipe std::vector bp_resources; std::vector> bp_provides; std::vector> bp_requires; + std::vector> bp_excludes; }; #endif // RECIPE_H diff --git a/src/savegame_json.cpp b/src/savegame_json.cpp index 5033d096ca17e..e1ecfd43b5cd7 100644 --- a/src/savegame_json.cpp +++ b/src/savegame_json.cpp @@ -3081,6 +3081,15 @@ void basecamp::serialize( JsonOut &json ) const json.end_object(); } json.end_array(); + json.member( "in_progress" ); + json.start_array(); + for( const auto working : expansion.second.in_progress ) { + json.start_object(); + json.member( "id", working.first ); + json.member( "amount", working.second ); + json.end_object(); + } + json.end_array(); json.member( "pos", expansion.second.pos ); json.end_object(); } @@ -3129,6 +3138,13 @@ void basecamp::deserialize( JsonIn &jsin ) if( e.provides.find( initial_provide ) == e.provides.end() ) { e.provides[ initial_provide ] = 1; } + JsonArray in_progress_arr = edata.get_array( "in_progress" ); + while( in_progress_arr.has_more() ) { + JsonObject in_progress_data = in_progress_arr.next_object(); + std::string id = in_progress_data.get_string( "id" ); + int amount = in_progress_data.get_int( "amount" ); + e.in_progress[ id ] = amount; + } edata.read( "pos", e.pos ); expansions[ dir ] = e; if( dir != "[B]" ) {