From 2e869da4b871131fd9a5a0030b1874ba5db35749 Mon Sep 17 00:00:00 2001 From: Vollch Date: Tue, 24 Oct 2023 02:31:55 +0300 Subject: [PATCH] feat(content): specials placement rework (#3438) * Specials placement rework * Generate new special during game start, if required one can't be found * Subway connections of specials will connect to global stations network * Obsolete non rotatabe stations --- data/json/obsoletion/terrains.json | 4 +- .../overmap/multitile_city_buildings.json | 8 +- data/json/overmap/overmap_connections.json | 5 +- .../overmap/overmap_special/specials.json | 21 +- .../overmap_terrain_transportation.json | 4 +- data/json/overmap/special_locations.json | 5 + src/options.cpp | 5 + src/overmap.cpp | 339 +++++------------- src/overmap.h | 27 +- src/overmap_special.h | 16 - src/overmapbuffer.cpp | 66 ++-- src/savegame.cpp | 3 +- src/start_location.cpp | 31 ++ tests/overmap_test.cpp | 58 +-- 14 files changed, 187 insertions(+), 405 deletions(-) diff --git a/data/json/obsoletion/terrains.json b/data/json/obsoletion/terrains.json index 5dda924ca0e0..56557304db91 100644 --- a/data/json/obsoletion/terrains.json +++ b/data/json/obsoletion/terrains.json @@ -18,7 +18,9 @@ "mine_entrance", "mine_shaft", "spiral", - "spiral_hub" + "spiral_hub", + "underground_sub_station", + "sewer_sub_station" ] } ] diff --git a/data/json/overmap/multitile_city_buildings.json b/data/json/overmap/multitile_city_buildings.json index 4124a2b07788..2e3e00b8597a 100644 --- a/data/json/overmap/multitile_city_buildings.json +++ b/data/json/overmap/multitile_city_buildings.json @@ -955,7 +955,13 @@ "locations": [ "land" ], "overmaps": [ { "point": [ 0, 0, 0 ], "overmap": "sub_station_north" }, - { "point": [ 0, 0, 1 ], "overmap": "sub_station_roof_north" } + { "point": [ 0, 0, 1 ], "overmap": "sub_station_roof_north" }, + { "point": [ 0, 0, -1 ], "overmap": "sewer_sub_station_north" }, + { "point": [ 0, 0, -2 ], "overmap": "underground_sub_station_north" } + ], + "connections": [ + { "point": [ 0, -1, -2 ], "connection": "subway_tunnel", "from": [ 0, 0, -2 ] }, + { "point": [ 0, 1, -2 ], "connection": "subway_tunnel", "from": [ 0, 0, -2 ] } ] }, { diff --git a/data/json/overmap/overmap_connections.json b/data/json/overmap/overmap_connections.json index 1eef0a716c1f..c7ab01fe63bd 100644 --- a/data/json/overmap/overmap_connections.json +++ b/data/json/overmap/overmap_connections.json @@ -22,7 +22,10 @@ "type": "overmap_connection", "id": "subway_tunnel", "default_terrain": "subway", - "subtypes": [ { "terrain": "subway", "locations": [ "subterranean_subway" ], "flags": [ "ORTHOGONAL" ] } ] + "subtypes": [ + { "terrain": "underground_sub_station", "locations": [ "underground_sub_station" ], "flags": [ "ORTHOGONAL" ] }, + { "terrain": "subway", "locations": [ "subterranean_subway" ], "flags": [ "ORTHOGONAL" ] } + ] }, { "type": "overmap_connection", diff --git a/data/json/overmap/overmap_special/specials.json b/data/json/overmap/overmap_special/specials.json index 3d6df7fb5ca5..0d86c40f0588 100644 --- a/data/json/overmap/overmap_special/specials.json +++ b/data/json/overmap/overmap_special/specials.json @@ -317,8 +317,7 @@ ], "locations": [ "forest" ], "city_distance": [ 25, -1 ], - "occurrences": [ 50, 100 ], - "//": "Inflated chance, effective rate ~40%", + "occurrences": [ 40, 100 ], "flags": [ "CLASSIC", "WILDERNESS", "UNIQUE", "ELECTRIC_GRID" ] }, { @@ -4758,8 +4757,7 @@ ], "city_distance": [ 5, 40 ], "city_sizes": [ 6, -1 ], - "occurrences": [ 70, 100 ], - "//": "Inflated chance, effective rate ~40%", + "occurrences": [ 40, 100 ], "flags": [ "CLASSIC", "UNIQUE", "ELECTRIC_GRID" ] }, { @@ -4857,8 +4855,7 @@ "locations": [ "wilderness" ], "city_distance": [ 5, 20 ], "city_sizes": [ 6, -1 ], - "occurrences": [ 40, 100 ], - "//": "Inflated chance, in effect 15%", + "occurrences": [ 15, 100 ], "flags": [ "CLASSIC", "UNIQUE" ] }, { @@ -6464,8 +6461,7 @@ ], "locations": [ "land" ], "city_distance": [ 5, -1 ], - "occurrences": [ 95, 100 ], - "//": "Inflated chance, effective rate ~ 30%", + "occurrences": [ 30, 100 ], "flags": [ "CLASSIC", "FARM", "UNIQUE" ] }, { @@ -6654,8 +6650,7 @@ "connections": [ { "point": [ 2, -1, 0 ], "connection": "local_road", "from": [ 2, 0, 0 ] } ], "city_distance": [ 5, 40 ], "city_sizes": [ 4, -1 ], - "occurrences": [ 75, 100 ], - "//": "Inflated chance, effective rate ~55%", + "occurrences": [ 55, 100 ], "flags": [ "CLASSIC", "UNIQUE", "ELECTRIC_GRID" ] }, { @@ -6743,8 +6738,7 @@ "locations": [ "wilderness" ], "city_distance": [ 8, 40 ], "city_sizes": [ 6, -1 ], - "occurrences": [ 50, 100 ], - "//": "Inflated chance, effective rate ~30%", + "occurrences": [ 30, 100 ], "flags": [ "CLASSIC", "URBAN", "UNIQUE", "ELECTRIC_GRID" ] }, { @@ -7006,8 +7000,7 @@ "connections": [ { "point": [ 3, -2, 0 ], "from": [ 3, -1, 0 ], "connection": "local_road" } ], "city_distance": [ 5, -1 ], "city_sizes": [ 4, -1 ], - "occurrences": [ 45, 100 ], - "//": "Inflated chance, in effect ~10%", + "occurrences": [ 10, 100 ], "flags": [ "MILITARY", "UNIQUE", "ELECTRIC_GRID" ] }, { diff --git a/data/json/overmap/overmap_terrain/overmap_terrain_transportation.json b/data/json/overmap/overmap_terrain/overmap_terrain_transportation.json index e574cbae7f04..f89d81ccbf21 100644 --- a/data/json/overmap/overmap_terrain/overmap_terrain_transportation.json +++ b/data/json/overmap/overmap_terrain/overmap_terrain_transportation.json @@ -279,7 +279,7 @@ "color": "yellow", "see_cost": 5, "extras": "subway", - "flags": [ "KNOWN_UP", "KNOWN_DOWN", "NO_ROTATE" ] + "flags": [ "KNOWN_UP", "KNOWN_DOWN" ] }, { "type": "overmap_terrain", @@ -289,7 +289,7 @@ "color": "yellow", "see_cost": 5, "extras": "subway", - "flags": [ "KNOWN_UP", "NO_ROTATE" ] + "flags": [ "KNOWN_UP" ] }, { "type": "overmap_terrain", diff --git a/data/json/overmap/special_locations.json b/data/json/overmap/special_locations.json index f169a6bb4ac6..794d4caf368a 100644 --- a/data/json/overmap/special_locations.json +++ b/data/json/overmap/special_locations.json @@ -99,5 +99,10 @@ "type": "overmap_location", "id": "lake_shore", "terrains": [ "lake_shore" ] + }, + { + "type": "overmap_location", + "id": "underground_sub_station", + "terrains": [ "underground_sub_station" ] } ] diff --git a/src/options.cpp b/src/options.cpp index 0c0edbb2e364..cc697816da00 100644 --- a/src/options.cpp +++ b/src/options.cpp @@ -2243,6 +2243,11 @@ void options_manager::add_options_world_default() 0, 8, 4 ); + add( "SPECIALS_DENSITY", world_default, translate_marker( "Overmap specials density" ), + translate_marker( "A scaling factor that determines density of overmap specials." ), + 0.0, 10.0, 1, 0.1 + ); + add( "SPAWN_DENSITY", world_default, translate_marker( "Spawn rate scaling factor" ), translate_marker( "A scaling factor that determines density of monster spawns." ), 0.0, 50.0, 1.0, 0.1 diff --git a/src/overmap.cpp b/src/overmap.cpp index c5b938ba3d2d..dcc6970f86d0 100644 --- a/src/overmap.cpp +++ b/src/overmap.cpp @@ -505,19 +505,6 @@ void overmap_specials::finalize() void overmap_specials::check_consistency() { - const size_t max_count = ( OMAPX / OMSPEC_FREQ ) * ( OMAPY / OMSPEC_FREQ ); - const size_t actual_count = std::accumulate( specials.get_all().begin(), specials.get_all().end(), - static_cast< size_t >( 0 ), - []( size_t sum, const overmap_special & elem ) { - return sum + ( elem.flags.count( "UNIQUE" ) ? static_cast( 0 ) : static_cast( ( - std::max( elem.occurrences.min, 0 ) ) ) ) ; - } ); - - if( actual_count > max_count ) { - debugmsg( "There are too many mandatory overmap specials (%d > %d). Some of them may not be placed.", - actual_count, max_count ); - } - specials.check(); } @@ -999,12 +986,11 @@ void overmap_special::load( const JsonObject &jo, const std::string &src ) mandatory( jo, was_loaded, "overmaps", terrains ); optional( jo, was_loaded, "locations", default_locations ); + optional( jo, was_loaded, "connections", connections ); if( is_special ) { mandatory( jo, was_loaded, "occurrences", occurrences ); - optional( jo, was_loaded, "connections", connections ); - assign( jo, "city_sizes", city_size, strict ); assign( jo, "city_distance", city_distance, strict ); } @@ -1695,6 +1681,21 @@ bool overmap::generate_sub( const int z ) std::vector lab_train_points; std::vector central_lab_train_points; std::vector mine_points; + + // Connect subways of cities + const overmap_connection_id subway_tunnel( "subway_tunnel" ); + for( const auto &elem : cities ) { + if( subway_tunnel->has( ter( tripoint_om_omt( elem.pos, z ) ) ) ) { + subway_points.emplace_back( elem.pos ); + } + } + // Connect outer subways + for( const auto &p : connections_out[subway_tunnel] ) { + if( p.z() == z ) { + subway_points.emplace_back( p.xy() ); + } + } + // These are so common that it's worth checking first as int. const oter_id skip_above[5] = { oter_id( "empty_rock" ), oter_id( "forest" ), oter_id( "field" ), @@ -1753,15 +1754,7 @@ bool overmap::generate_sub( const int z ) continue; } - if( is_ot_match( "sub_station", oter_ground, ot_match_type::type ) && z == -1 ) { - ter_set( p, oter_id( "sewer_sub_station" ) ); - requires_sub = true; - } else if( is_ot_match( "sub_station", oter_ground, ot_match_type::type ) && z == -2 ) { - ter_set( p, oter_id( "subway_isolated" ) ); - subway_points.emplace_back( i, j - 1 ); - subway_points.emplace_back( i, j ); - subway_points.emplace_back( i, j + 1 ); - } else if( oter_above == "road_nesw_manhole" ) { + if( oter_above == "road_nesw_manhole" ) { ter_set( p, oter_id( "sewer_isolated" ) ); sewer_points.emplace_back( i, j ); } else if( oter_above == "sewage_treatment" ) { @@ -1878,18 +1871,10 @@ bool overmap::generate_sub( const int z ) create_real_train_lab_points( lab_train_points, subway_lab_train_points ); create_real_train_lab_points( central_lab_train_points, subway_lab_train_points ); - const overmap_connection_id subway_tunnel( "subway_tunnel" ); - subway_points.insert( subway_points.end(), subway_lab_train_points.begin(), subway_lab_train_points.end() ); connect_closest_points( subway_points, z, *subway_tunnel ); - for( auto &i : subway_points ) { - if( is_ot_match( "sub_station", ter( tripoint_om_omt( i, z + 2 ) ), ot_match_type::type ) ) { - ter_set( tripoint_om_omt( i, z ), oter_id( "underground_sub_station" ) ); - } - } - // The first lab point is adjacent to a lab, set it a depot (as long as track was actually laid). const auto create_train_depots = [this, z, &subway_tunnel]( const oter_id & train_type, const std::vector &train_points ) { @@ -4455,257 +4440,101 @@ void overmap::place_special( } } -om_special_sectors get_sectors( const int sector_width ) +int overmap::place_special_attempt( const overmap_special &special, const int max, + std::vector &points, const bool must_be_unexplored ) { - std::vector res; - - res.reserve( ( OMAPX / sector_width ) * ( OMAPY / sector_width ) ); - for( int x = 0; x < OMAPX; x += sector_width ) { - for( int y = 0; y < OMAPY; y += sector_width ) { - res.emplace_back( x, y ); - } + if( max < 1 ) { + return 0; } - std::shuffle( res.begin(), res.end(), rng_get_engine() ); - return om_special_sectors{ res, sector_width }; -} - -bool overmap::place_special_attempt( - overmap_special_batch &enabled_specials, const point_om_omt §or, - const int sector_width, const bool place_optional, const bool must_be_unexplored ) -{ - const tripoint_om_omt p{ - rng( sector.x(), sector.x() + sector_width - 1 ), - rng( sector.y(), sector.y() + sector_width - 1 ), - 0}; - const city &nearest_city = get_nearest_city( p ); + int placed = 0; + for( auto p = points.begin(); p != points.end(); ) { + const city &nearest_city = get_nearest_city( *p ); - std::shuffle( enabled_specials.begin(), enabled_specials.end(), rng_get_engine() ); - for( auto iter = enabled_specials.begin(); iter != enabled_specials.end(); ++iter ) { - const auto &special = *iter->special_details; - // If we haven't finished placing minimum instances of all specials, - // skip specials that are at their minimum count already. - if( !place_optional && iter->instances_placed >= special.occurrences.min ) { - continue; - } // City check is the fastest => it goes first. - if( !special.can_belong_to_city( p, nearest_city ) ) { + if( !special.can_belong_to_city( *p, nearest_city ) ) { + p++; continue; } // See if we can actually place the special there. - const auto rotation = random_special_rotation( special, p, must_be_unexplored ); + const auto rotation = random_special_rotation( special, *p, must_be_unexplored ); if( rotation == om_direction::type::invalid ) { + p++; continue; } + place_special( special, *p, rotation, nearest_city, false, must_be_unexplored ); - place_special( special, p, rotation, nearest_city, false, must_be_unexplored ); - - if( ++iter->instances_placed >= special.occurrences.max ) { - enabled_specials.erase( iter ); - } - - return true; - } - - return false; -} - -void overmap::place_specials_pass( overmap_special_batch &enabled_specials, - om_special_sectors §ors, const bool place_optional, const bool must_be_unexplored ) -{ - // Walk over sectors in random order, to minimize "clumping". - std::shuffle( sectors.sectors.begin(), sectors.sectors.end(), rng_get_engine() ); - for( auto it = sectors.sectors.begin(); it != sectors.sectors.end(); ) { - const size_t attempts = 10; - bool placed = false; - for( size_t i = 0; i < attempts; ++i ) { - if( place_special_attempt( enabled_specials, *it, sectors.sector_width, place_optional, - must_be_unexplored ) ) { - placed = true; - it = sectors.sectors.erase( it ); - if( enabled_specials.empty() ) { - return; // Job done. Bail out. - } - break; - } - } - - if( !placed ) { - it++; + // Sometimes points can be reused with different rotation + // wa want to avoid that for better dispertion + p = points.erase( p ); + if( ++placed >= max ) { + break; } } + return placed; } -// Split map into sections, iterate through sections iterate through specials, -// check if special is valid pick & place special. -// When a sector is populated it's removed from the list, -// and when a special reaches max instances it is also removed. +// Iterate over overmap searching for valid locations, and placing specials void overmap::place_specials( overmap_special_batch &enabled_specials ) { - // Calculate if this overmap has any lake terrain--if it doesn't, we should just - // completely skip placing any lake specials here since they'll never place and if - // they're mandatory they just end up causing us to spiral out into adjacent overmaps - // which probably don't have lakes either. - bool overmap_has_lake = false; - for( int z = -OVERMAP_DEPTH; z <= OVERMAP_HEIGHT && !overmap_has_lake; z++ ) { - for( int x = 0; x < OMAPX && !overmap_has_lake; x++ ) { - for( int y = 0; y < OMAPY && !overmap_has_lake; y++ ) { - overmap_has_lake = ter( { x, y, z } )->is_lake(); - } - } - } - - om_special_sectors sectors = get_sectors( OMSPEC_FREQ ); - - // At true center, central lab is truly mandatory and can't be pushed to neighbors + // Sort specials be they sizes - placing big things is faster + // and easier while we have most of map still empty, and also + // that central lab will have top priority bool is_true_center = pos() == point_abs_om(); - if( is_true_center ) { - std::vector truly_mandatory_specials; - // Ugly hack. TODO: Un-ugly - for( const auto &s : overmap_specials::get_all() ) { - if( s.flags.count( "ENDGAME" ) > 0 ) { - truly_mandatory_specials.push_back( &s ); - } - } - overmap_special_batch truly_mandatory_batch( point_abs_om(), truly_mandatory_specials ); - place_specials_pass( truly_mandatory_batch, sectors, false, false ); - // Add instances_placed from truly mandatory batch to regular batch, to avoid double placement - // But only if they exist in both - we don't want endgame specials to be pushed to neighbor OMs - for( const overmap_special_placement &truly_mandatory_placement : truly_mandatory_batch ) { - const overmap_special_id &special_id = truly_mandatory_placement.special_details->id; - auto iter_normal = std::find_if( enabled_specials.begin(), - enabled_specials.end(), [special_id]( const overmap_special_placement & pl ) { - return pl.special_details->id == special_id; - } ); - if( iter_normal != enabled_specials.end() ) { - iter_normal->instances_placed += truly_mandatory_placement.instances_placed; - } - } + const auto special_weight = [&is_true_center]( const overmap_special * s ) { + return s->terrains.size() * ( is_true_center && s->flags.count( "ENDGAME" ) ? 1000 : 1 ); + }; + std::sort( enabled_specials.begin(), enabled_specials.end(), + [&special_weight]( const overmap_special_placement & a, const overmap_special_placement & b ) { + return special_weight( a.special_details ) > special_weight( b.special_details ); + } ); - for( auto iter = enabled_specials.begin(); iter != enabled_specials.end(); ) { - if( iter->special_details->flags.count( "ENDGAME" ) > 0 ) { - iter = enabled_specials.erase( iter ); - continue; + // Prepare overmap points + std::vector lake_points; + std::vector land_points; + for( int x = 0; x < OMAPX; x++ ) { + for( int y = 0; y < OMAPY; y++ ) { + const tripoint_om_omt p = {x, y, 0}; + if( ter( p )->is_lake() ) { + lake_points.push_back( p ); + } else { + land_points.push_back( p ); } - ++iter; } } - for( auto iter = enabled_specials.begin(); iter != enabled_specials.end(); ) { - // If this special has the LAKE flag and the overmap doesn't have any - // lake terrain, then remove this special from the candidates for this - // overmap. - if( iter->special_details->flags.count( "LAKE" ) > 0 && !overmap_has_lake ) { - iter = enabled_specials.erase( iter ); - continue; - } + // Calculate water to land ratio to normalize specials occurencies + // we don't want to dump all specials on overmap covered by water + float lake_rate = lake_points.size() / static_cast( OMAPX * OMAPY ) * + get_option( "SPECIALS_DENSITY" ); + float land_rate = land_points.size() / static_cast( OMAPX * OMAPY ) * + get_option( "SPECIALS_DENSITY" ); - // Endgame specials were placed above, don't let UNIQUE overwrite that - if( iter->special_details->flags.count( "UNIQUE" ) > 0 ) { - const int min = iter->special_details->occurrences.min; - const int max = iter->special_details->occurrences.max; + // Shuffle all points to distribute specials across the overmap + std::shuffle( lake_points.begin(), lake_points.end(), rng_get_engine() ); + std::shuffle( land_points.begin(), land_points.end(), rng_get_engine() ); + // And here we go + for( auto &iter : enabled_specials ) { + const overmap_special &special = *iter.special_details; - if( x_in_y( min, max ) ) { - // Min and max are overloaded to be the chance of occurrence, - // so reset instances placed to one short of max so we don't place several. - iter->instances_placed = max - 1; - } else { - iter = enabled_specials.erase( iter ); - continue; - } - } - ++iter; - } - // Bail out early if we have nothing to place. - if( enabled_specials.empty() ) { - return; - } + const int min = special.occurrences.min; + const int max = special.occurrences.max; - std::shuffle( enabled_specials.begin(), enabled_specials.end(), rng_get_engine() ); - - // First, place the mandatory specials to ensure that all minimum instance - // counts are met. - place_specials_pass( enabled_specials, sectors, false, false ); - - // Snapshot remaining specials, which will be the optional specials and - // any unplaced mandatory specials. By passing a copy into the creation of - // the adjacent overmaps, we ensure that when we unwind the overmap creation - // back to filling in our non-mandatory specials for this overmap, we won't - // count the placement of the specials in those maps when looking for optional - // specials to place here. - overmap_special_batch custom_overmap_specials = overmap_special_batch( enabled_specials ); - - // Check for any unplaced mandatory specials, and if there are any, attempt to - // place them on adajacent uncreated overmaps. - if( std::any_of( custom_overmap_specials.begin(), custom_overmap_specials.end(), - []( overmap_special_placement placement ) { - return placement.instances_placed < - placement.special_details->occurrences.min; -} ) ) { - // Randomly select from among the nearest uninitialized overmap positions. - int previous_distance = 0; - std::vector nearest_candidates; - // Since this starts at enabled_specials::origin, it will only place new overmaps - // in the 5x5 area surrounding the initial overmap, bounding the amount of work we will do. - for( const point_abs_om &candidate_addr : closest_points_first( - custom_overmap_specials.get_origin(), 2 ) ) { - if( !overmap_buffer.has( candidate_addr ) ) { - int current_distance = square_dist( pos(), candidate_addr ); - if( nearest_candidates.empty() || current_distance == previous_distance ) { - nearest_candidates.push_back( candidate_addr ); - previous_distance = current_distance; - } else { - break; - } - } - } - if( !nearest_candidates.empty() ) { - std::shuffle( nearest_candidates.begin(), nearest_candidates.end(), rng_get_engine() ); - point_abs_om new_om_addr = nearest_candidates.front(); - overmap_buffer.create_custom_overmap( new_om_addr, custom_overmap_specials ); - } else { - add_msg( _( "Unable to place all configured specials, some missions may fail to initialize." ) ); - } - } - // Then fill in non-mandatory specials. - place_specials_pass( enabled_specials, sectors, true, false ); - - // Clean up... - // Because we passed a copy of the specials for placement in adjacent overmaps rather than - // the original, but our caller is concerned with whether or not they were placed at all, - // regardless of whether we placed them or our callee did, we need to reconcile the placement - // that we did of the optional specials with the placement our callee did of optional - // and mandatory. - - // Make a lookup of our callee's specials after processing. - // Because specials are removed from the list once they meet their maximum - // occurrences, this will only contain those which have not yet met their - // maximum. - std::map processed_specials; - for( auto &elem : custom_overmap_specials ) { - processed_specials[elem.special_details->id] = elem.instances_placed; - } - - // Loop through the specials we started with. - for( auto it = enabled_specials.begin(); it != enabled_specials.end(); ) { - // Determine if this special is still in our callee's list of specials... - std::map::iterator iter = processed_specials.find( - it->special_details->id ); - if( iter != processed_specials.end() ) { - // ... and if so, increment the placement count to reflect the callee's. - it->instances_placed += ( iter->second - it->instances_placed ); - - // If, after incrementing the placement count, we're at our max, remove - // this special from our list. - if( it->instances_placed >= it->special_details->occurrences.max ) { - it = enabled_specials.erase( it ); - } else { - it++; - } + const bool is_lake = special.flags.count( "LAKE" ); + const float rate = is_true_center && special.flags.count( "ENDGAME" ) ? 1 : + ( is_lake ? lake_rate : land_rate ); + + int amount_to_place; + if( special.flags.count( "UNIQUE" ) ) { + int chance = roll_remainder( min * rate ); + amount_to_place = x_in_y( chance, max ) ? 1 : 0; } else { - // This special is no longer in our callee's list, which means it was completely - // placed, and we can remove it from our list. - it = enabled_specials.erase( it ); + // Number of instances normalized to terrain ratio + float real_max = std::max( static_cast( min ), max * rate ); + amount_to_place = roll_remainder( rng_float( min, real_max ) ); } + + iter.instances_placed += place_special_attempt( special, + amount_to_place, ( is_lake ? lake_points : land_points ), false ); } } diff --git a/src/overmap.h b/src/overmap.h index 5ad4b88cddb3..7908ba6ad20d 100644 --- a/src/overmap.h +++ b/src/overmap.h @@ -42,7 +42,6 @@ class npc; class overmap_connection; class overmap_special; class overmap_special_batch; -struct om_special_sectors; struct regional_settings; template struct enum_traits; @@ -469,24 +468,18 @@ class overmap * @param enabled_specials specifies what specials to place, and tracks how many have been placed. **/ void place_specials( overmap_special_batch &enabled_specials ); - /** - * Walk over the overmap and attempt to place specials. - * @param enabled_specials vector of objects that track specials being placed. - * @param sectors sectors in which to attempt placement. - * @param place_optional restricts attempting to place specials that have met their minimum count in the first pass. - */ - void place_specials_pass( overmap_special_batch &enabled_specials, - om_special_sectors §ors, bool place_optional, bool must_be_unexplored ); /** - * Attempts to place specials within a sector. - * @param enabled_specials vector of objects that track specials being placed. - * @param sector sector identifies the location where specials are being placed. - * @param place_optional restricts attempting to place specials that have met their minimum count in the first pass. - */ - bool place_special_attempt( - overmap_special_batch &enabled_specials, const point_om_omt §or, int sector_width, - bool place_optional, bool must_be_unexplored ); + * Iterate over given points, placing specials if possible. + * @param special The overmap special to place. + * @param max Maximum amount of specials to place. + * @param points Vector of allowed origins for new specials, used points will be erased. + * @param must_be_unexplored If true, will require that all of the + * terrains where the special would be placed are unexplored. + * @returns Actual amount of placed specials. + **/ + int place_special_attempt( const overmap_special &special, const int max, + std::vector &points, const bool must_be_unexplored ); void place_mongroups(); void place_radios(); diff --git a/src/overmap_special.h b/src/overmap_special.h index 2d5d65a8482c..7a0b69b2d8af 100644 --- a/src/overmap_special.h +++ b/src/overmap_special.h @@ -30,11 +30,6 @@ class overmap_special; // Overmap specials--these are "special encounters," dungeons, nests, etc. // This specifies how often and where they may be placed. -// OMSPEC_FREQ determines the length of the side of the square in which each -// overmap special will be placed. At OMSPEC_FREQ 6, the overmap is divided -// into 900 squares; lots of space for interesting stuff! -static constexpr int OMSPEC_FREQ = 15; - struct overmap_special_spawns : public overmap_spawns { numeric_interval radius; @@ -127,17 +122,6 @@ void load( const JsonObject &jo, const std::string &src ); } // namespace city_buildings -struct om_special_sectors { - std::vector sectors; - int sector_width; -}; - -/** -* Gets a collection of sectors and their width for usage in placing overmap specials. -* @param sector_width used to divide the OMAPX by OMAPY map into sectors. -*/ -om_special_sectors get_sectors( int sector_width ); - // Wrapper around an overmap special to track progress of placing specials. struct overmap_special_placement { int instances_placed; diff --git a/src/overmapbuffer.cpp b/src/overmapbuffer.cpp index a7f4d3d3e741..bf67cd7dadf7 100644 --- a/src/overmapbuffer.cpp +++ b/src/overmapbuffer.cpp @@ -15,6 +15,7 @@ #include "cata_utility.h" #include "character_id.h" #include "color.h" +#include "map_iterator.h" #include "numeric_interval.h" #include "coordinate_conversions.h" #include "coordinates.h" @@ -1602,10 +1603,6 @@ bool overmapbuffer::place_special( const overmap_special_id &special_id, return false; } - // Force our special to occur just once when we're spawning it here. - special.occurrences.min = 1; - special.occurrences.max = 1; - // Figure out the longest side of the special for purposes of determining our sector size // when attempting placements. const auto calculate_longest_side = [&special]() { @@ -1624,58 +1621,39 @@ bool overmapbuffer::place_special( const overmap_special_id &special_id, const int special_longest_side = std::max( min_max_x.second->p.x - min_max_x.first->p.x, min_max_y.second->p.y - min_max_y.first->p.y ) + 1; - // If our longest side is greater than the OMSPEC_FREQ, just use that instead. - return std::min( special_longest_side, OMSPEC_FREQ ); + return special_longest_side; }; const int longest_side = calculate_longest_side(); - // Predefine our sectors to search in. - om_special_sectors sectors = get_sectors( longest_side ); - // Get all of the overmaps within the defined radius of the center. for( const auto &om : get_overmaps_near( project_to( center ), omt_to_sm_copy( radius ) ) ) { - // Build an overmap_special_batch for the special on this overmap. - std::vector specials; - specials.push_back( &special ); - overmap_special_batch batch( om->pos(), specials ); - - // Filter the sectors to those which are in in range of our center point, so - // that we don't end up creating specials in areas that are outside of our radius, - // since the whole point is to create a special that is within the parameters. - std::vector sector_points_in_range; - std::copy_if( sectors.sectors.begin(), sectors.sectors.end(), - std::back_inserter( sector_points_in_range ), [&]( point_om_omt & p ) { - const point_abs_omt global_sector_point = project_combine( om->pos(), p ); - // We'll include this sector if it's within our radius. We reduce the radius by - // the length of the longest side of our special so that we don't end up in a - // scenario where one overmap terrain of the special is within the radius but the - // rest of it is outside the radius (due to size, rotation, etc), which would - // then result in us placing the special but then not finding it later if we - // search using the same radius value we used in placing it. - return square_dist( global_sector_point, center.xy() ) <= radius - longest_side; - } ); - om_special_sectors sectors_in_range {sector_points_in_range, sectors.sector_width}; + // We'll include points that within our radius. We reduce the radius by + // the length of the longest side of our special so that we don't end up in a + // scenario where one overmap terrain of the special is within the radius but the + // rest of it is outside the radius (due to size, rotation, etc), which would + // then result in us placing the special but then not finding it later if we + // search using the same radius value we used in placing it. + std::vector points_in_range; + for( const tripoint_abs_omt &p : points_in_radius( center, radius - longest_side ) ) { + point_abs_om overmap; + tripoint_om_omt omt_within_overmap; + std::tie( overmap, omt_within_overmap ) = project_remain( p ); + if( overmap == om->pos() ) { + points_in_range.push_back( omt_within_overmap ); + } + } - // Attempt to place the specials using our batch and sectors. We - // require they be placed in unexplored terrain right now. - om->place_specials_pass( batch, sectors_in_range, true, true ); + // First points will have a priority, let's randomize placement + std::shuffle( points_in_range.begin(), points_in_range.end(), rng_get_engine() ); - // The place special pass will erase specials that have reached their - // maximum number of instances so first check if its been erased. - if( batch.empty() ) { + // Attempt to place the specials using filtered points. We + // require they be placed in unexplored terrain right now. + if( om->place_special_attempt( special, 1, points_in_range, true ) > 0 ) { return true; } - - // Hasn't been erased, so lets check placement counts. - for( const auto &special_placement : batch ) { - if( special_placement.instances_placed > 0 ) { - // It was placed, lets get outta here. - return true; - } - } } // If we got this far, we've failed to make the placement. diff --git a/src/savegame.cpp b/src/savegame.cpp index 877627944d02..b06126f5ef57 100644 --- a/src/savegame.cpp +++ b/src/savegame.cpp @@ -373,7 +373,8 @@ void overmap::convert_terrain( if( old == "fema" || old == "fema_entrance" || old == "fema_1_3" || old == "fema_2_1" || old == "fema_2_2" || old == "fema_2_3" || old == "fema_3_1" || old == "fema_3_2" || old == "fema_3_3" || - old == "mine_entrance" ) { + old == "mine_entrance" || old == "underground_sub_station" || + old == "sewer_sub_station" ) { ter_set( pos, oter_id( old + "_north" ) ); } else if( old.compare( 0, 10, "mass_grave" ) == 0 ) { ter_set( pos, oter_id( "field" ) ); diff --git a/src/start_location.cpp b/src/start_location.cpp index e977e5da3598..d14afd61b32b 100644 --- a/src/start_location.cpp +++ b/src/start_location.cpp @@ -23,6 +23,7 @@ #include "mapdata.h" #include "output.h" #include "overmap.h" +#include "overmap_special.h" #include "overmapbuffer.h" #include "player.h" #include "pldata.h" @@ -222,6 +223,36 @@ tripoint_abs_omt start_location::find_player_initial_location() const return project_combine( omp, omtstart ); } } + // Still no location, let's spawn one + static const tripoint_om_omt om_mid{ OMAPX / 2, OMAPY / 2, 0 }; + + // Check terrains where we're allowed to spawn + for( const auto &loc : _omt_types ) { + + // Look for special having that terrain + for( const auto &special : overmap_specials::get_all() ) { + if( std::none_of( special.terrains.begin(), + special.terrains.end(), [&loc]( const overmap_special_terrain & t ) { + return is_ot_match( loc.first, t.terrain, loc.second ); + } ) ) { + continue; + } + + // Look for place where it can be spawned + for( const point_abs_om &omp : overmaps ) { + const tripoint_abs_omt abs_mid = project_combine( omp, om_mid ); + if( overmap_buffer.place_special( special.id, abs_mid, OMAPX / 2 ) ) { + + // Now try to find what we spawned + const tripoint_abs_omt start = overmap_buffer.find_closest( abs_mid, loc.first, OMAPX / 2, false, + loc.second ); + if( start != overmap::invalid_tripoint ) { + return start; + } + } + } + } + } // Should never happen, if it does we messed up. popup( _( "Unable to generate a valid starting location %s [%s] in a radius of %d overmaps, please report this failure." ), name(), id.str(), radius ); diff --git a/tests/overmap_test.cpp b/tests/overmap_test.cpp index bf6a654112c1..9f0350a6d183 100644 --- a/tests/overmap_test.cpp +++ b/tests/overmap_test.cpp @@ -51,6 +51,9 @@ TEST_CASE( "default_overmap_generation_always_succeeds", "[slow]" ) overmap_buffer.create_custom_overmap( candidate_addr, test_specials ); for( const auto &special_placement : test_specials ) { auto special = special_placement.special_details; + if( special->flags.count( "UNIQUE" ) > 0 ) { + continue; + } INFO( "In attempt #" << overmaps_to_construct << " failed to place " << special->id.str() ); CHECK( special->occurrences.min <= special_placement.instances_placed ); @@ -60,58 +63,6 @@ TEST_CASE( "default_overmap_generation_always_succeeds", "[slow]" ) } } } - -TEST_CASE( "default_overmap_generation_has_non_mandatory_specials_at_origin", "[slow]" ) -{ - clear_all_state(); - const point_abs_om origin{}; - - overmap_special mandatory; - overmap_special optional; - - // Get some specific overmap specials so we can assert their presence later. - // This should probably be replaced with some custom specials created in - // memory rather than tying this test to these, but it works for now... - for( const auto &elem : overmap_specials::get_all() ) { - if( elem.id == overmap_special_id( "Cabin" ) ) { - optional = elem; - } else if( elem.id == overmap_special_id( "Lab" ) ) { - mandatory = elem; - } - } - - // Make this mandatory special impossible to place. - mandatory.city_size.min = 999; - - // Construct our own overmap_special_batch containing only our single mandatory - // and single optional special, so we can make some assertions. - std::vector specials; - specials.push_back( &mandatory ); - specials.push_back( &optional ); - overmap_special_batch test_specials = overmap_special_batch( origin, specials ); - - // Run the overmap creation, which will try to place our specials. - overmap_buffer.create_custom_overmap( origin, test_specials ); - - // Get the origin overmap... - overmap *test_overmap = overmap_buffer.get_existing( origin ); - - // ...and assert that the optional special exists on this map. - bool found_optional = false; - for( int x = 0; x < OMAPX; ++x ) { - for( int y = 0; y < OMAPY; ++y ) { - const oter_id t = test_overmap->ter( { x, y, 0 } ); - if( t->id == "cabin" || - t->id == "cabin_north" || t->id == "cabin_east" || - t->id == "cabin_south" || t->id == "cabin_west" ) { - found_optional = true; - } - } - } - - INFO( "Failed to place optional special on origin " ); - CHECK( found_optional == true ); -} namespace { @@ -174,7 +125,8 @@ TEST_CASE( "is_ot_match", "[overmap][terrain]" ) // Does not match if base type does not match CHECK_FALSE( is_ot_match( "lab", oter_id( "central_lab" ), ot_match_type::type ) ); - CHECK_FALSE( is_ot_match( "sub_station", oter_id( "sewer_sub_station" ), ot_match_type::type ) ); + CHECK_FALSE( is_ot_match( "sub_station", oter_id( "sewer_sub_station_north" ), + ot_match_type::type ) ); } SECTION( "prefix match" ) {