diff --git a/doc/EFFECT_ON_CONDITION.md b/doc/EFFECT_ON_CONDITION.md index 301fb8881312e..b256ca25998a5 100644 --- a/doc/EFFECT_ON_CONDITION.md +++ b/doc/EFFECT_ON_CONDITION.md @@ -1248,7 +1248,7 @@ Runs a query, allowing you to pick specific tile around. When picked, stores coo ### `map_in_city` - type: location string or [variable object](#variable-object) -- return true if the location is in a city +- return true if the location is in the bounds of a city at or above z-1 #### Valid talkers: @@ -1265,6 +1265,19 @@ Check the location is in a city. }, ``` +Each time the avatar enters an OMT display a message as to whether or not they're in a city. +``` + { + "type": "effect_on_condition", + "id": "EOC_TEST_IS_IN_CITY", + "eoc_type": "EVENT", + "required_event": "avatar_enters_omt", + "condition": { "map_in_city": { "mutator": "u_loc_relative", "target": "(0,0,0)" } }, + "effect": [ { "u_message": "You are in a city OMT.", "type": "good" } ], + "false_effect": [ { "u_message": "You are NOT in a city OMT.", "type": "bad" } ] + }, +``` + ### `player_see_u`, `player_see_npc` - type: simple string - return true if player can see alpha or beta talker diff --git a/src/condition.cpp b/src/condition.cpp index 367f57241172f..b712839647cf8 100644 --- a/src/condition.cpp +++ b/src/condition.cpp @@ -1760,10 +1760,15 @@ conditional_t::func f_map_in_city( const JsonObject &jo, std::string_view member { str_or_var target = get_str_or_var( jo.get_member( member ), member, true ); return [target]( dialogue const & d ) { - tripoint_abs_ms target_pos = tripoint_abs_ms( tripoint::from_string( target.evaluate( d ) ) ); - city_reference c = overmap_buffer.closest_city( project_to( target_pos ) ); - c.distance = rl_dist( c.abs_sm_pos, project_to( target_pos ) ); - return c && c.get_distance_from_bounds() <= 0; + tripoint_abs_omt target_pos = project_to( tripoint_abs_ms( tripoint::from_string( + target.evaluate( d ) ) ) ); + + // TODO: Remove this in favour of a seperate condition for location z-level that can be used in conjunction with this map_in_city as needed + if( target_pos.z() < -1 ) { + return false; + } + + return overmap_buffer.is_in_city( target_pos ); }; } diff --git a/src/omdata.h b/src/omdata.h index 0903513536eaa..1397865b3c098 100644 --- a/src/omdata.h +++ b/src/omdata.h @@ -659,7 +659,7 @@ class overmap_special /** @returns true if this special requires a city */ bool requires_city() const; /** @returns whether the special at specified tripoint can belong to the specified city. */ - bool can_belong_to_city( const tripoint_om_omt &p, const city &cit ) const; + bool can_belong_to_city( const tripoint_om_omt &p, const city &cit, const overmap &omap ) const; const cata::flat_set &get_flags() const { return flags_; } diff --git a/src/overmap.cpp b/src/overmap.cpp index 4189143bfc706..44db92f751bfa 100644 --- a/src/overmap.cpp +++ b/src/overmap.cpp @@ -2888,7 +2888,8 @@ bool overmap_special::requires_city() const constraints_.city_distance.max < std::max( OMAPX, OMAPY ); } -bool overmap_special::can_belong_to_city( const tripoint_om_omt &p, const city &cit ) const +bool overmap_special::can_belong_to_city( const tripoint_om_omt &p, const city &cit, + const overmap &omap ) const { if( !requires_city() ) { return true; @@ -2896,7 +2897,13 @@ bool overmap_special::can_belong_to_city( const tripoint_om_omt &p, const city & if( !cit || !constraints_.city_size.contains( cit.size ) ) { return false; } - return constraints_.city_distance.contains( cit.get_distance_from( p ) - ( cit.size ) ); + if( constraints_.city_distance.max > std::max( OMAPX, OMAPY ) ) { + // Only care that we're more than min away from a city + return !omap.distance_to_city( p, constraints_.city_distance.min ); + } + const std::optional dist = omap.distance_to_city( p, constraints_.city_distance.max ); + // Found a city within max and it's greater than min away + return !!dist && constraints_.city_distance.min < *dist; } bool overmap_special::has_flag( const std::string &flag ) const @@ -4180,6 +4187,83 @@ void overmap::clear_connections_out() connections_out.clear(); } +bool overmap::is_in_city( const tripoint_om_omt &p ) const +{ + if( !city_tiles.empty() ) { + return city_tiles.find( p.xy() ) != city_tiles.end(); + } else { + // Legacy handling + return distance_to_city( p ) == 0; + } + +} + +std::optional overmap::distance_to_city( const tripoint_om_omt &p, + int max_dist_to_check ) const +{ + if( !city_tiles.empty() ) { + for( int i = 0; i <= max_dist_to_check; i++ ) { + for( const tripoint_om_omt &tile : closest_points_first( p, i, i ) ) { + if( is_in_city( tile ) ) { + return i; + } + } + } + } else { + // Legacy handling + const city &nearest_city = get_nearest_city( p ); + if( !!nearest_city ) { + // 0 if within city + return std::max( 0, nearest_city.get_distance_from( p ) - nearest_city.size ); + } + } + return {}; +} + +void overmap::flood_fill_city_tiles() +{ + std::unordered_set visited; + // simplifies bounds checking + const half_open_rectangle omap_bounds( point_om_omt( 0, 0 ), point_om_omt( OMAPX, + OMAPY ) ); + + // Look through every point on the overmap + for( int y = 0; y < OMAPY; y++ ) { + for( int x = 0; x < OMAPX; x++ ) { + point_om_omt checked( x, y ); + // If we already looked at it in a previous flood-fill, ignore it + if( visited.find( checked ) != visited.end() ) { + continue; + } + // Is the area connected to this point enclosed by city_tiles? + bool enclosed = true; + // Predicate for flood-fill. Also detects if any point flood-filled to borders the edge + // of the overmap and is thus not enclosed + const auto is_unchecked = [&enclosed, &visited, &omap_bounds, this]( const point_om_omt & pt ) { + if( city_tiles.find( pt ) != visited.end() ) { + return false; + } + // We hit the edge of the overmap! We're free! + if( !omap_bounds.contains( pt ) ) { + enclosed = false; + return false; + } + return true; + }; + // All the points connected to this point that aren't part of a city + std::vector area = ff::point_flood_fill_4_connected( checked, visited, is_unchecked ); + if( !enclosed ) { + continue; + } + // They are enclosed, and so should be considered part of the city. + city_tiles.reserve( city_tiles.size() + area.size() ); + for( const point_om_omt &pt : area ) { + city_tiles.insert( pt ); + } + } + } +} + static std::map oter_id_migrations; static std::map> camp_migration_map; @@ -5825,6 +5909,7 @@ void overmap::place_cities() if( ter( p ) == settings->default_oter[OVERMAP_DEPTH] ) { placement_attempts = 0; ter_set( p, oter_road_nesw ); // every city starts with an intersection + city_tiles.insert( c ); tmp.pos = p.xy(); tmp.size = size; } @@ -5833,6 +5918,7 @@ void overmap::place_cities() tmp = random_entry( cities_to_place ); p = tripoint_om_omt( tmp.pos, 0 ); ter_set( tripoint_om_omt( tmp.pos, 0 ), oter_road_nesw ); + city_tiles.insert( tmp.pos ); } if( placement_attempts == 0 ) { cities.push_back( tmp ); @@ -5846,6 +5932,7 @@ void overmap::place_cities() } while( ( cur_dir = om_direction::turn_right( cur_dir ) ) != start_dir ); } } + flood_fill_city_tiles(); } overmap_special_id overmap::pick_random_building_to_place( int town_dist, int town_size, @@ -5907,7 +5994,11 @@ void overmap::place_building( const tripoint_om_omt &p, om_direction::type dir, const overmap_special_id building_tid = pick_random_building_to_place( town_dist, town.size, placed_unique_buildings ); if( can_place_special( *building_tid, building_pos, building_dir, false ) ) { - place_special( *building_tid, building_pos, building_dir, town, false, false ); + std::vector used_tripoints = place_special( *building_tid, building_pos, + building_dir, town, false, false ); + for( const tripoint_om_omt &p : used_tripoints ) { + city_tiles.insert( p.xy() ); + } if( building_tid->has_flag( "CITY_UNIQUE" ) ) { placed_unique_buildings.emplace( building_tid ); } @@ -6329,7 +6420,7 @@ static pf::directed_path straight_path( const point_om_omt &source } pf::directed_path overmap::lay_out_street( const overmap_connection &connection, - const point_om_omt &source, om_direction::type dir, size_t len ) const + const point_om_omt &source, om_direction::type dir, size_t len ) { const tripoint_om_omt from( source, 0 ); // See if we need to make another one "step" further. @@ -6382,6 +6473,7 @@ pf::directed_path overmap::lay_out_street( const overmap_connectio break; } + city_tiles.insert( pos.xy() ); ++actual_len; if( actual_len > 1 && connection.has( ter_id ) ) { @@ -7021,7 +7113,7 @@ bool overmap::place_special_attempt( 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, *this ) ) { continue; } // See if we can actually place the special there. diff --git a/src/overmap.h b/src/overmap.h index 75c669ef910a2..eb044b28846d7 100644 --- a/src/overmap.h +++ b/src/overmap.h @@ -333,7 +333,16 @@ class overmap void clear_connections_out(); void place_special_forced( const overmap_special_id &special_id, const tripoint_om_omt &p, om_direction::type dir ); + // Whether the tripoint's point is true in city_tiles + bool is_in_city( const tripoint_om_omt &p ) const; + // Returns the distance to the nearest city_tile within max_dist_to_check or std::nullopt if there isn't one + std::optional distance_to_city( const tripoint_om_omt &p, + int max_dist_to_check = OMAPX ) const; private: + // Any point that is part of or surrounded by a city + std::unordered_set city_tiles; + // Fill in any gaps in city_tiles that don't connect to the map edge + void flood_fill_city_tiles(); std::multimap zg; // NOLINT(cata-serialize) public: /** Unit test enablers to check if a given mongroup is present. */ @@ -507,7 +516,7 @@ class overmap const point_om_omt &dest, int z, bool must_be_unexplored ) const; pf::directed_path lay_out_street( const overmap_connection &connection, const point_om_omt &source, - om_direction::type dir, size_t len ) const; + om_direction::type dir, size_t len ); public: void build_connection( const overmap_connection &connection, const pf::directed_path &path, int z, diff --git a/src/overmapbuffer.cpp b/src/overmapbuffer.cpp index 4d6c826eae562..3485195934ed0 100644 --- a/src/overmapbuffer.cpp +++ b/src/overmapbuffer.cpp @@ -1682,6 +1682,15 @@ bool overmapbuffer::is_safe( const tripoint_abs_omt &p ) return true; } +bool overmapbuffer::is_in_city( const tripoint_abs_omt &p ) +{ + point_abs_om overmap_pos; + tripoint_om_omt potential_city_tile; + std::tie( overmap_pos, potential_city_tile ) = project_remain( p ); + overmap &target_overmap = get( overmap_pos ); + return target_overmap.is_in_city( potential_city_tile ); +} + std::optional> overmapbuffer::place_special( const overmap_special &special, const tripoint_abs_omt &origin, om_direction::type dir, const bool must_be_unexplored, const bool force ) diff --git a/src/overmapbuffer.h b/src/overmapbuffer.h index 64bd771262907..c495804f866d7 100644 --- a/src/overmapbuffer.h +++ b/src/overmapbuffer.h @@ -231,6 +231,10 @@ class overmapbuffer * If there are any, it's not safe. */ bool is_safe( const tripoint_abs_omt &p ); + /** + * Check if the tripoint is part of or surrounded by a city, ignoring z-level + */ + bool is_in_city( const tripoint_abs_omt &p ); /** * Move the tracking mark of the given vehicle. diff --git a/src/savegame.cpp b/src/savegame.cpp index 58cdcfda99d49..014ca9bdbc81a 100644 --- a/src/savegame.cpp +++ b/src/savegame.cpp @@ -513,6 +513,8 @@ void overmap::unserialize( const JsonObject &jsobj ) } cities.push_back( new_city ); } + } else if( name == "city_tiles" ) { + om_member.read( city_tiles ); } else if( name == "connections_out" ) { om_member.read( connections_out ); } else if( name == "roads_out" ) { @@ -1251,6 +1253,9 @@ void overmap::serialize( std::ostream &fout ) const json.end_array(); fout << std::endl; + json.member( "city_tiles", city_tiles ); + fout << std::endl; + json.member( "connections_out", connections_out ); fout << std::endl;