diff --git a/src/a_star.h b/src/a_star.h new file mode 100644 index 0000000000000..3581ebf254654 --- /dev/null +++ b/src/a_star.h @@ -0,0 +1,274 @@ +#pragma once +#ifndef CATA_SRC_A_STAR_H +#define CATA_SRC_A_STAR_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +template , typename BestPathMap = std::unordered_map>> + class AStarState +{ +public: + void reset( const Node &start, Cost cost ); + + std::optional get_next( Cost max ); + + template + void generate_neighbors( const Node ¤t, StateCostFn &&s_cost_fn, TransitionCostFn &&t_cost_fn, + HeuristicFn &&heuristic_fn, NeighborsFn &&neighbors_fn ); + + bool has_path_to( const Node &end ) const; + + Cost path_cost( const Node &end ) const; + + std::vector path_to( const Node &end ) const; + +private: + struct FirstElementGreaterThan { + template + bool operator()( const std::tuple &lhs, const std::tuple &rhs ) const { + return std::get<0>( lhs ) > std::get<0>( rhs ); + } + }; + + using FrontierNode = std::tuple; + using FrontierQueue = + std::priority_queue, FirstElementGreaterThan>; + + VisitedSet visited_; + BestPathMap best_paths_; + FrontierQueue frontier_; +}; + +template , typename BestStateMap = std::unordered_map>> + class AStarPathfinder +{ +public: + template + std::vector find_path( Cost max, const Node &from, const Node &to, + StateCostFn &&s_cost_fn, TransitionCostFn &&t_cost_fn, HeuristicFn &&heuristic_fn, + NeighborsFn &&neighbors_fn ); + +private: + AStarState state_; +}; + +template , typename BestStateMap = std::unordered_map>> + class BidirectionalAStarPathfinder +{ +public: + template + std::vector find_path( Cost max, const Node &from, const Node &to, + StateCostFn &&s_cost_fn, TransitionCostFn &&t_cost_fn, HeuristicFn &&heuristic_fn, + NeighborsFn &&neighbors_fn ); + +private: + AStarState forward_; + AStarState backward_; +}; + +// Implementation Details + +template +void AStarState::reset( const Node &start, Cost cost ) +{ + visited_.clear(); + best_paths_.clear(); + + // priority_queue doesn't have a clear method, so we cannot reuse it, and it may + // get quite large, so we explicitly create the underlying container and reserve + // space beforehand. + std::vector queue_data; + queue_data.reserve( 400 ); + frontier_ = FrontierQueue( FirstElementGreaterThan(), std::move( queue_data ) ); + + best_paths_.try_emplace( start, 0, start ); + frontier_.emplace( cost, start ); +} + +template +bool AStarState::has_path_to( const Node &end ) const +{ + return best_paths_.count( end ); +} + +template +Cost AStarState::path_cost( const Node &end ) const +{ + return best_paths_.at( end ).first; +} + +template +std::vector AStarState::path_to( const Node &end ) const +{ + std::vector result; + if( has_path_to( end ) ) { + Node current = end; + while( best_paths_.at( current ).first != 0 ) { + result.push_back( current ); + current = best_paths_.at( current ).second; + } + std::reverse( result.begin(), result.end() ); + } + return result; +} + +template +std::optional AStarState::get_next( Cost max ) +{ + while( !frontier_.empty() ) { + auto [cost, current] = frontier_.top(); + frontier_.pop(); + + if( cost >= max ) { + return std::nullopt; + } + + if( const auto& [_, inserted] = visited_.emplace( current ); !inserted ) { + continue; + } + return current; + } + return std::nullopt; +} + +template +template +void AStarState::generate_neighbors( + const Node ¤t, StateCostFn &&s_cost_fn, TransitionCostFn &&t_cost_fn, + HeuristicFn &&heuristic_fn, + NeighborsFn &&neighbors_fn ) +{ + // Can't use structured bindings here due to a defect in Clang 10. + const std::pair &best_path = best_paths_[current]; + const Cost current_cost = best_path.first; + const Node ¤t_parent = best_path.second; + neighbors_fn( current_parent, current, [this, &s_cost_fn, &t_cost_fn, &heuristic_fn, ¤t, + current_cost]( const Node & neighbour ) { + if( visited_.count( neighbour ) ) { + return; + } + if( const std::optional s_cost = s_cost_fn( neighbour ) ) { + if( const std::optional t_cost = t_cost_fn( current, neighbour ) ) { + const auto& [iter, _] = best_paths_.try_emplace( neighbour, std::numeric_limits::max(), + Node() ); + auto& [best_cost, parent] = *iter; + const Cost new_cost = current_cost + *s_cost + *t_cost; + if( new_cost < best_cost ) { + best_cost = new_cost; + parent = current; + const Cost estimated_cost = new_cost + heuristic_fn( neighbour ); + frontier_.emplace( estimated_cost, neighbour ); + } + } + } else { + visited_.emplace( neighbour ); + } + } ); +} + +template +template +std::vector AStarPathfinder::find_path( + Cost max_cost, const Node &from, const Node &to, StateCostFn &&s_cost_fn, + TransitionCostFn &&t_cost_fn, + HeuristicFn &&heuristic_fn, NeighborsFn &&neighbors_fn ) +{ + if( !s_cost_fn( from ) || !s_cost_fn( to ) ) { + return {}; + } + + state_.reset( from, heuristic_fn( from, to ) ); + while( const std::optional current = state_.get_next( max_cost ) ) { + if( *current == to ) { + return state_.path_to( to ); + } + state_.generate_neighbors( *current, s_cost_fn, t_cost_fn, [&heuristic_fn, + to]( const Node & from ) { + return heuristic_fn( from, to ); + }, neighbors_fn ); + } + return {}; +} + + +template +template +std::vector BidirectionalAStarPathfinder::find_path( + Cost max_cost, const Node &from, const Node &to, StateCostFn &&s_cost_fn, + TransitionCostFn &&t_cost_fn, + HeuristicFn &&heuristic_fn, NeighborsFn &&neighbors_fn ) +{ + if( !s_cost_fn( from ) || !s_cost_fn( to ) ) { + return {}; + } + + // The full cost is not used since that would result in paths that are up to 2x longer than + // intended. Half the cost cannot be used, since there is no guarantee that both searches + // proceed at the same pace. 2/3rds the cost is a fine balance between the two, and has the + // effect of the worst case still visiting less states than normal A*. + const Cost partial_max_cost = 2 * max_cost / 3; + forward_.reset( from, heuristic_fn( from, to ) ); + backward_.reset( to, heuristic_fn( to, from ) ); + for( ;; ) { + const std::optional f_current_state = forward_.get_next( partial_max_cost ); + if( !f_current_state ) { + break; + } + const std::optional b_current_state = backward_.get_next( partial_max_cost ); + if( !b_current_state ) { + break; + } + + bool f_links = backward_.has_path_to( *f_current_state ); + bool b_links = forward_.has_path_to( *b_current_state ); + + if( f_links && b_links ) { + const Cost f_cost = forward_.path_cost( *f_current_state ) + backward_.path_cost( + *f_current_state ); + const Cost b_cost = forward_.path_cost( *b_current_state ) + backward_.path_cost( + *b_current_state ); + if( b_cost < f_cost ) { + f_links = false; + } else { + b_links = false; + } + } + + if( f_links || b_links ) { + const Node &midpoint = f_links ? *f_current_state : *b_current_state; + std::vector forward_path = forward_.path_to( midpoint ); + std::vector backward_path = backward_.path_to( midpoint ); + if( backward_path.empty() ) { + return forward_path; + } + backward_path.pop_back(); + std::for_each( backward_path.rbegin(), backward_path.rend(), [&forward_path]( const Node & node ) { + forward_path.push_back( node ); + } ); + forward_path.push_back( to ); + return forward_path; + } + + forward_.generate_neighbors( *f_current_state, s_cost_fn, t_cost_fn, [&heuristic_fn, + &to]( const Node & from ) { + return heuristic_fn( from, to ); + }, neighbors_fn ); + backward_.generate_neighbors( *b_current_state, s_cost_fn, [&t_cost_fn]( const Node & from, + const Node & to ) { + return t_cost_fn( to, from ); + }, [&heuristic_fn, &from]( const Node & to ) { + return heuristic_fn( to, from ); + }, neighbors_fn ); + } + return {}; +} + +#endif // CATA_SRC_A_STAR_H diff --git a/src/activity_actor.cpp b/src/activity_actor.cpp index e1627bff18df5..a53c95e7b1ef2 100644 --- a/src/activity_actor.cpp +++ b/src/activity_actor.cpp @@ -7188,8 +7188,7 @@ void unload_loot_activity_actor::do_turn( player_activity &act, Character &you ) return; } std::vector route; - route = here.route( you.pos_bub(), src_loc, you.get_pathfinding_settings(), - you.get_path_avoid() ); + route = here.route( you.pos_bub(), src_loc, you.get_pathfinding_settings() ); if( route.empty() ) { // can't get there, can't do anything, skip it continue; @@ -7225,8 +7224,7 @@ void unload_loot_activity_actor::do_turn( player_activity &act, Character &you ) // get either direct route or route to nearest adjacent tile if // source tile is impassable if( here.passable( src_loc ) ) { - route = here.route( you.pos_bub(), src_loc, you.get_pathfinding_settings(), - you.get_path_avoid() ); + route = here.route( you.pos_bub(), src_loc, you.get_pathfinding_settings() ); } else { // impassable source tile (locker etc.), // get route to nearest adjacent tile instead diff --git a/src/activity_handlers.cpp b/src/activity_handlers.cpp index 47c8d578ae9e3..2165f465f06fd 100644 --- a/src/activity_handlers.cpp +++ b/src/activity_handlers.cpp @@ -2911,8 +2911,7 @@ void activity_handlers::travel_do_turn( player_activity *act, Character *you ) } } const std::vector route_to = - here.route( you->pos_bub(), centre_sub, you->get_pathfinding_settings(), - you->get_path_avoid() ); + here.route( you->pos_bub(), centre_sub, you->get_pathfinding_settings() ); if( !route_to.empty() ) { const activity_id act_travel = ACT_TRAVELLING; you->set_destination( route_to, player_activity( act_travel ) ); @@ -3598,8 +3597,7 @@ static void perform_zone_activity_turn( const tripoint_bub_ms &tile_loc = here.bub_from_abs( tile ); std::vector route = - here.route( you->pos_bub(), tile_loc, you->get_pathfinding_settings(), - you->get_path_avoid() ); + here.route( you->pos_bub(), tile_loc, you->get_pathfinding_settings() ); if( route.size() > 1 ) { route.pop_back(); diff --git a/src/activity_item_handling.cpp b/src/activity_item_handling.cpp index 5fb76e5cfc4da..e86f8d9835310 100644 --- a/src/activity_item_handling.cpp +++ b/src/activity_item_handling.cpp @@ -674,7 +674,7 @@ std::vector route_adjacent( const Character &you, const tripoin const auto &avoid = you.get_path_avoid(); for( const tripoint_bub_ms &tp : sorted ) { std::vector route = - here.route( you.pos_bub(), tp, you.get_pathfinding_settings(), avoid ); + here.route( you.pos_bub(), tp, you.get_pathfinding_settings() ); if( !route.empty() ) { return route; @@ -752,7 +752,7 @@ static std::vector route_best_workbench( } for( const tripoint_bub_ms &tp : sorted ) { std::vector route = - here.route( you.pos_bub(), tp, you.get_pathfinding_settings(), avoid ); + here.route( you.pos_bub(), tp, you.get_pathfinding_settings() ); if( !route.empty() ) { return route; @@ -2055,8 +2055,7 @@ void activity_on_turn_move_loot( player_activity &act, Character &you ) return; } std::vector route; - route = here.route( you.pos_bub(), src_loc, you.get_pathfinding_settings(), - you.get_path_avoid() ); + route = here.route( you.pos_bub(), src_loc, you.get_pathfinding_settings() ); if( route.empty() ) { // can't get there, can't do anything, skip it continue; @@ -2112,8 +2111,7 @@ void activity_on_turn_move_loot( player_activity &act, Character &you ) // get either direct route or route to nearest adjacent tile if // source tile is impassable if( here.passable( src_loc ) ) { - route = here.route( you.pos_bub(), src_loc, you.get_pathfinding_settings(), - you.get_path_avoid() ); + route = here.route( you.pos_bub(), src_loc, you.get_pathfinding_settings() ); } else { // impassable source tile (locker etc.), // get route to nearest adjacent tile instead diff --git a/src/character.h b/src/character.h index 86a2c133c1e87..ed516bb5d6c4b 100644 --- a/src/character.h +++ b/src/character.h @@ -3264,8 +3264,8 @@ class Character : public Creature, public visitable /** Returns the player's modified base movement cost */ int run_cost( int base_cost, bool diag = false ) const; - const pathfinding_settings &get_pathfinding_settings() const override; - std::function get_path_avoid() const override; + virtual const pathfinding_settings &get_pathfinding_settings() const; + virtual std::function get_path_avoid() const; /** * Get all hostile creatures currently visible to this player. */ diff --git a/src/coordinates.h b/src/coordinates.h index 1499ebf5c34ac..4ce8f38d960bb 100644 --- a/src/coordinates.h +++ b/src/coordinates.h @@ -710,6 +710,13 @@ inline int octile_dist( const coords::coord_point +inline float octile_dist_exact( const coords::coord_point &loc1, + const coords::coord_point &loc2 ) +{ + return octile_dist_exact( loc1.raw(), loc2.raw() ); +} + template direction direction_from( const coords::coord_point &loc1, const coords::coord_point &loc2 ) diff --git a/src/creature.h b/src/creature.h index f19152b67202a..e2a9608f1d7d5 100644 --- a/src/creature.h +++ b/src/creature.h @@ -945,11 +945,6 @@ class Creature : public viewer virtual units::mass weight_capacity() const; - /** Returns settings for pathfinding. */ - virtual const pathfinding_settings &get_pathfinding_settings() const = 0; - /** Returns a set of points we do not want to path through. */ - virtual std::function get_path_avoid() const = 0; - bool underwater; void draw( const catacurses::window &w, const point_bub_ms &origin, bool inverted ) const; // TODO: Get rid of the untyped overload diff --git a/src/debug_menu.cpp b/src/debug_menu.cpp index d9e04a6cda62b..bcb044796a78d 100644 --- a/src/debug_menu.cpp +++ b/src/debug_menu.cpp @@ -3427,8 +3427,7 @@ static void set_automove() // TODO: fix point types auto rt = get_map().route( player_character.pos_bub(), tripoint_bub_ms( *dest ), - player_character.get_pathfinding_settings(), - player_character.get_path_avoid() ); + player_character.get_pathfinding_settings() ); if( !rt.empty() ) { player_character.set_destination( rt ); } else { diff --git a/src/do_turn.cpp b/src/do_turn.cpp index faabb59d44833..408703bb5c39a 100644 --- a/src/do_turn.cpp +++ b/src/do_turn.cpp @@ -268,25 +268,29 @@ void monmove() for( monster &critter : g->all_monsters() ) { // Critters in impassable tiles get pushed away, unless it's not impassable for them - if( !critter.is_dead() && m.impassable( critter.pos() ) && !critter.can_move_to( critter.pos() ) ) { - dbg( D_ERROR ) << "game:monmove: " << critter.name() - << " can't move to its location! (" << critter.posx() - << ":" << critter.posy() << ":" << critter.posz() << "), " - << m.tername( critter.pos() ); - add_msg_debug( debugmode::DF_MONSTER, "%s can't move to its location! (%d,%d,%d), %s", - critter.name(), - critter.posx(), critter.posy(), critter.posz(), m.tername( critter.pos() ) ); - bool okay = false; - for( const tripoint &dest : m.points_in_radius( critter.pos(), 3 ) ) { - if( critter.can_move_to( dest ) && g->is_empty( dest ) ) { - critter.setpos( dest ); - okay = true; - break; + + if( !critter.is_dead() && m.impassable( critter.pos() ) ) { + const PathfindingSettings settings = critter.get_pathfinding_settings(); + if( !critter.can_move_to( critter.pos(), settings ) ) { + dbg( D_ERROR ) << "game:monmove: " << critter.name() + << " can't move to its location! (" << critter.posx() + << ":" << critter.posy() << ":" << critter.posz() << "), " + << m.tername( critter.pos() ); + add_msg_debug( debugmode::DF_MONSTER, "%s can't move to its location! (%d,%d,%d), %s", + critter.name(), + critter.posx(), critter.posy(), critter.posz(), m.tername( critter.pos() ) ); + bool okay = false; + for( const tripoint &dest : m.points_in_radius( critter.pos(), 3 ) ) { + if( critter.can_move_to( dest, settings ) && g->is_empty( dest ) ) { + critter.setpos( dest ); + okay = true; + break; + } + } + if( !okay ) { + // die of "natural" cause (overpopulation is natural) + critter.die( nullptr ); } - } - if( !okay ) { - // die of "natural" cause (overpopulation is natural) - critter.die( nullptr ); } } diff --git a/src/game.cpp b/src/game.cpp index d120ecc6894e7..0bf6db60cf794 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -5009,7 +5009,7 @@ static bool can_place_monster( const monster &mon, const tripoint &p ) if( creatures.creature_at( p ) ) { return false; } - return mon.will_move_to( p ) && mon.know_danger_at( p ); + return mon.can_move_to( p ); } static bool can_place_npc( const tripoint &p ) @@ -7456,10 +7456,9 @@ std::optional> game::safe_route_to( Character &who, if( is_dangerous_tile( p.raw() ) ) { continue; } + // TODO: Check the new pathfinding settings returns an equivalent result. const route_t route = here.route( who.pos_bub(), p, - who.get_pathfinding_settings(), [this]( const tripoint & p ) { - return is_dangerous_tile( p ); - } ); + who.get_pathfinding_settings() ); if( route.empty() ) { continue; // no route } @@ -12324,43 +12323,51 @@ void game::start_hauling( const tripoint &pos ) u.assign_activity( actor ); } -std::optional game::find_or_make_stairs( map &mp, const int z_after, bool &rope_ladder, - bool peeking, const tripoint &pos ) +std::optional game::find_stairs( const map &mp, const int z_after, const tripoint &pos ) { - const bool is_avatar = u.pos() == pos; - const int omtilesz = SEEX * 2; - real_coords rc( mp.getabs( pos.xy() ) ); - tripoint omtile_align_start( mp.getlocal( rc.begin_om_pos() ), z_after ); - tripoint omtile_align_end( omtile_align_start + point( -1 + omtilesz, -1 + omtilesz ) ); - // Try to find the stairs. - std::optional stairs; - int best = INT_MAX; const int movez = z_after - pos.z; const bool going_down_1 = movez == -1; - const bool going_up_1 = movez == 1; // If there are stairs on the same x and y as we currently are, use those if( going_down_1 && mp.has_flag( ter_furn_flag::TFLAG_GOES_UP, pos + tripoint_below ) ) { - stairs.emplace( pos + tripoint_below ); + return pos + tripoint_below; } + const bool going_up_1 = movez == 1; if( going_up_1 && mp.has_flag( ter_furn_flag::TFLAG_GOES_DOWN, pos + tripoint_above ) ) { - stairs.emplace( pos + tripoint_above ); + return pos + tripoint_above; } + // We did not find stairs directly above or below, so search the map for them - // If there's empty space right below us, we can just go down that way. - if( !stairs.has_value() && get_map().tr_at( u.pos() ) != tr_ledge ) { - for( const tripoint &dest : mp.points_in_rectangle( omtile_align_start, omtile_align_end ) ) { - if( rl_dist( u.pos(), dest ) <= best && - ( ( going_down_1 && mp.has_flag( ter_furn_flag::TFLAG_GOES_UP, dest ) ) || - ( going_up_1 && ( mp.has_flag( ter_furn_flag::TFLAG_GOES_DOWN, dest ) || - mp.ter( dest ) == ter_t_manhole_cover ) ) || - ( ( movez == 2 || movez == -2 ) && mp.ter( dest ) == ter_t_elevator ) ) ) { - stairs.emplace( dest ); - best = rl_dist( u.pos(), dest ); - } + int best = INT_MAX; + std::optional stairs; + const int omtilesz = SEEX * 2 - 1; + real_coords rc( mp.getabs( pos.xy() ) ); + tripoint omtile_align_start( mp.getlocal( rc.begin_om_pos() ), z_after ); + tripoint omtile_align_end( omtile_align_start + point( omtilesz, omtilesz ) ); + for( const tripoint &dest : mp.points_in_rectangle( omtile_align_start, omtile_align_end ) ) { + if( rl_dist( pos, dest ) <= best && + ( ( going_down_1 && mp.has_flag( ter_furn_flag::TFLAG_GOES_UP, dest ) ) || + ( going_up_1 && ( mp.has_flag( ter_furn_flag::TFLAG_GOES_DOWN, dest ) || + mp.ter( dest ) == ter_t_manhole_cover ) ) || + ( ( movez == 2 || movez == -2 ) && mp.ter( dest ) == ter_t_elevator ) ) ) { + stairs.emplace( dest ); + best = rl_dist( pos, dest ); } } + return stairs; +} + +std::optional game::find_or_make_stairs( const map &mp, const int z_after, + bool &rope_ladder, + bool peeking, const tripoint &pos ) +{ + const bool is_avatar = u.pos() == pos; + const int movez = z_after - pos.z; + + // Try to find the stairs. + std::optional stairs = find_stairs( mp, z_after, pos ); + creature_tracker &creatures = get_creature_tracker(); if( stairs.has_value() ) { if( !is_avatar ) { diff --git a/src/game.h b/src/game.h index c48120acf4017..5f1b0c01334c8 100644 --- a/src/game.h +++ b/src/game.h @@ -289,8 +289,10 @@ class game /** Returns the other end of the stairs (if any). May query, affect u etc. * @param pos Disable queries and msgs if not the same position as player. */ - std::optional find_or_make_stairs( map &mp, int z_after, bool &rope_ladder, + std::optional find_stairs( const map &mp, int z_after, const tripoint &pos ); + std::optional find_or_make_stairs( const map &mp, int z_after, bool &rope_ladder, bool peeking, const tripoint &pos ); + /* * Prompt player on direction they want to climb up or down. */ diff --git a/src/handle_action.cpp b/src/handle_action.cpp index 3fa187fc9836a..559de84432014 100644 --- a/src/handle_action.cpp +++ b/src/handle_action.cpp @@ -2243,8 +2243,7 @@ bool game::do_regular_action( action_id &act, avatar &player_character, player_character.pos_bub() + dest_delta * ( SEEX - i ); destination_preview = m.route( player_character.pos_bub(), auto_travel_destination, - player_character.get_pathfinding_settings(), - player_character.get_path_avoid() ); + player_character.get_pathfinding_settings() ); if( !destination_preview.empty() ) { destination_preview.erase( destination_preview.begin() + 1, destination_preview.end() ); diff --git a/src/line.cpp b/src/line.cpp index d9c8c4697bfcd..1a1fa59468fb8 100644 --- a/src/line.cpp +++ b/src/line.cpp @@ -285,6 +285,19 @@ float octile_dist_exact( const point &loc1, const point &loc2 ) return d.x + d.y - 2 * mind + mind * M_SQRT2; } +float octile_dist_exact( const tripoint &from, const tripoint &to ) +{ + const tripoint d = ( from - to ).abs(); + const int min = std::min( d.x, std::min( d.y, d.z ) ); + const int max = std::max( d.x, std::max( d.y, d.z ) ); + const int mid = d.x + d.y + d.z - min - max; + + constexpr int one_axis = 1; + constexpr float two_axis = M_SQRT2; + constexpr float three_axis = 1.73205f; + return ( three_axis - two_axis ) * min + ( two_axis - one_axis ) * mid + one_axis * max; +} + units::angle atan2( const point &p ) { return units::atan2( p.y, p.x ); diff --git a/src/line.h b/src/line.h index c9025dcb602be..32c5c6a02c457 100644 --- a/src/line.h +++ b/src/line.h @@ -251,9 +251,10 @@ float rl_dist_exact( const tripoint &loc1, const tripoint &loc2 ); int manhattan_dist( const point &loc1, const point &loc2 ); // Travel distance between 2 points on a square grid, assuming diagonal moves -// cost sqrt(2) and cardinal moves cost 1. +// cost sqrt(2) or sqrt(3) and cardinal moves cost 1. int octile_dist( const point &loc1, const point &loc2, int multiplier = 1 ); float octile_dist_exact( const point &loc1, const point &loc2 ); +float octile_dist_exact( const tripoint &from, const tripoint &to ); // get angle of direction represented by point units::angle atan2( const point & ); diff --git a/src/magic_spell_effect.cpp b/src/magic_spell_effect.cpp index a8fa94bba92cd..b5d961472b048 100644 --- a/src/magic_spell_effect.cpp +++ b/src/magic_spell_effect.cpp @@ -1838,7 +1838,7 @@ void spell_effect::slime_split_on_death( const spell &sp, Creature &caster, shared_ptr_fast mon = make_shared_fast( slime_id ); mon->ammo = mon->type->starting_ammo; - if( mon->will_move_to( dest ) && mon->know_danger_at( dest ) ) { + if( mon->can_move_to( dest ) ) { if( monster *const blob = g->place_critter_around( mon, dest, 0 ) ) { sp.make_sound( dest, caster ); if( !permanent ) { diff --git a/src/map.cpp b/src/map.cpp index d5e263c12b333..c8c60ff15270c 100644 --- a/src/map.cpp +++ b/src/map.cpp @@ -257,10 +257,6 @@ map::map( int mapsize, bool zlev ) : my_MAPSIZE( mapsize ), my_HALF_MAPSIZE( map grid.resize( static_cast( my_MAPSIZE ) * my_MAPSIZE, nullptr ); } - for( auto &ptr : pathfinding_caches ) { - ptr = std::make_unique(); - } - dbg( D_INFO ) << "map::map(): my_MAPSIZE: " << my_MAPSIZE << " z-levels enabled:" << zlevels; traplocs.resize( trap::count() ); } @@ -1864,7 +1860,7 @@ bool map::furn_set( const tripoint_bub_ms &p, const furn_id &new_furniture, cons get_creature_tracker().invalidate_reachability_cache(); } // TODO: Limit to changes that affect move cost, traps and stairs - set_pathfinding_cache_dirty( p.raw() ); + set_pathfinding_cache_dirty( tripoint_bub_ms( p ) ); // Make sure the furniture falls if it needs to support_dirty( p.raw() ); @@ -2332,7 +2328,7 @@ bool map::ter_set( const tripoint &p, const ter_id &new_terrain, bool avoid_crea get_creature_tracker().invalidate_reachability_cache(); } // TODO: Limit to changes that affect move cost, traps and stairs - set_pathfinding_cache_dirty( p ); + set_pathfinding_cache_dirty( tripoint_bub_ms( p ) ); tripoint above( p.xy(), p.z + 1 ); // Make sure that if we supported something and no longer do so, it falls down @@ -3355,6 +3351,42 @@ int map::bash_rating( const int str, const tripoint &p, const bool allow_floor ) return bash_rating_internal( str, furniture, terrain, allow_floor, veh, part ); } +std::optional> map::bash_range( const tripoint &p, + const bool allow_floor ) const +{ + if( !inbounds( p ) ) { + DebugLog( D_WARNING, D_MAP ) << "Looking for out-of-bounds is_bashable at " + << p.x << ", " << p.y << ", " << p.z; + return std::nullopt; + } + + if( const optional_vpart_position vp = veh_at( p ) ) { + if( const auto vpobst = vp->obstacle_at_part() ) { + const int bash_part = vpobst->part_index(); + // Car obstacle that isn't a door + // TODO: Account for armor + const int hp = vp->vehicle().part( bash_part ).hp(); + const int bash_min = hp / 20 + 1; + // Large max to discourage bashing. + const int bash_max = bash_min + 100; + return std::make_pair( bash_min, bash_max ); + } + } + + const furn_t &furniture = furn( p ).obj(); + ///\EFFECT_STR determines what furniture can be smashed + if( furniture.id && furniture.bash.str_max != -1 ) { + return std::make_pair( furniture.bash.str_min, furniture.bash.str_max ); + } + + const ter_t &terrain = ter( p ).obj(); + if( terrain.bash.str_max != -1 && ( !terrain.bash.bash_below || allow_floor ) ) { + return std::make_pair( terrain.bash.str_min, terrain.bash.str_max ); + } + + return std::nullopt; +} + // End of 3D bashable void map::make_rubble( const tripoint_bub_ms &p, const furn_id &rubble_type, const bool items, @@ -6738,7 +6770,7 @@ void map::on_field_modified( const tripoint &p, const field_type &fd_type ) } if( fd_type.is_dangerous() ) { - set_pathfinding_cache_dirty( p ); + set_pathfinding_cache_dirty( tripoint_bub_ms( p ) ); } // Ensure blood type fields don't hang in the air @@ -9003,10 +9035,11 @@ void map::spawn_monsters_submap_group( const tripoint &gp, mongroup &group, bool // Find horde's target submap for( monster &tmp : group.monsters ) { + const PathfindingSettings settings = tmp.get_pathfinding_settings(); for( int tries = 0; tries < 10 && !locations.empty(); tries++ ) { const tripoint local_pos = random_entry_removed( locations ); const tripoint_abs_ms abs_pos = get_map().getglobal( local_pos ); - if( !tmp.can_move_to( local_pos ) ) { + if( !tmp.can_move_to( local_pos, settings ) ) { continue; // target can not contain the monster } if( group.horde ) { @@ -10450,27 +10483,17 @@ const level_cache &map::access_cache( int zlev ) const return nullcache; } -pathfinding_cache::pathfinding_cache() -{ - dirty = true; -} - -pathfinding_cache &map::get_pathfinding_cache( int zlev ) const -{ - return *pathfinding_caches[zlev + OVERMAP_DEPTH]; -} - void map::set_pathfinding_cache_dirty( const int zlev ) { - if( inbounds_z( zlev ) ) { - get_pathfinding_cache( zlev ).dirty = true; + if( pathfinding_cache_ && inbounds_z( zlev ) ) { + pathfinding_cache_->invalidate( zlev ); } } -void map::set_pathfinding_cache_dirty( const tripoint &p ) +void map::set_pathfinding_cache_dirty( const tripoint_bub_ms &p ) { - if( inbounds( p ) ) { - get_pathfinding_cache( p.z ).dirty_points.insert( p.xy() ); + if( pathfinding_cache_ && inbounds( p ) ) { + pathfinding_cache_->invalidate( p ); } } @@ -10491,98 +10514,6 @@ void map::main_cleanup_override( bool over ) _main_cleanup_override = over; } -const pathfinding_cache &map::get_pathfinding_cache_ref( int zlev ) const -{ - if( !inbounds_z( zlev ) ) { - debugmsg( "Tried to get pathfinding cache for out of bounds z-level %d", zlev ); - return *pathfinding_caches[ OVERMAP_DEPTH ]; - } - pathfinding_cache &cache = get_pathfinding_cache( zlev ); - if( cache.dirty || !cache.dirty_points.empty() ) { - update_pathfinding_cache( zlev ); - } - - return cache; -} - -void map::update_pathfinding_cache( const tripoint &p ) const -{ - if( !inbounds( p ) ) { - return; - } - pathfinding_cache &cache = get_pathfinding_cache( p.z ); - pf_special cur_value = PF_NORMAL; - - const_maptile tile = maptile_at_internal( p ); - - const ter_t &terrain = tile.get_ter_t(); - const furn_t &furniture = tile.get_furn_t(); - const field &field = tile.get_field(); - const map &here = get_map(); - int part; - const vehicle *veh = veh_at_internal( p, part ); - - const int cost = move_cost_internal( furniture, terrain, field, veh, part ); - - if( cost > 2 ) { - cur_value |= PF_SLOW; - } else if( cost <= 0 ) { - cur_value |= PF_WALL; - if( terrain.has_flag( ter_furn_flag::TFLAG_CLIMBABLE ) ) { - cur_value |= PF_CLIMBABLE; - } - } - - if( veh != nullptr ) { - cur_value |= PF_VEHICLE; - } - - for( const auto &fld : tile.get_field() ) { - const field_entry &cur = fld.second; - if( cur.is_dangerous() ) { - cur_value |= PF_FIELD; - } - } - - if( ( !tile.get_trap_t().is_benign() || !terrain.trap.obj().is_benign() ) && - !here.has_vehicle_floor( p ) ) { - cur_value |= PF_TRAP; - } - - if( terrain.has_flag( ter_furn_flag::TFLAG_GOES_DOWN ) || - terrain.has_flag( ter_furn_flag::TFLAG_GOES_UP ) || - terrain.has_flag( ter_furn_flag::TFLAG_RAMP ) || terrain.has_flag( ter_furn_flag::TFLAG_RAMP_UP ) || - terrain.has_flag( ter_furn_flag::TFLAG_RAMP_DOWN ) ) { - cur_value |= PF_UPDOWN; - } - - if( terrain.has_flag( ter_furn_flag::TFLAG_SHARP ) && !here.has_vehicle_floor( p ) ) { - cur_value |= PF_SHARP; - } - - cache.special[p.x][p.y] = cur_value; -} - -void map::update_pathfinding_cache( int zlev ) const -{ - pathfinding_cache &cache = get_pathfinding_cache( zlev ); - - if( cache.dirty ) { - const int size = getmapsize(); - for( int x = 0; x < size * SEEX; ++x ) { - for( int y = 0; y < size * SEEX; ++y ) { - update_pathfinding_cache( { x, y, zlev } ); - } - } - cache.dirty = false; - } else { - for( const point &p : cache.dirty_points ) { - update_pathfinding_cache( { p, zlev } ); - } - } - cache.dirty_points.clear(); -} - void map::clip_to_bounds( tripoint &p ) const { clip_to_bounds( p.x, p.y, p.z ); diff --git a/src/map.h b/src/map.h index def2f349e1172..060a63bb98b69 100644 --- a/src/map.h +++ b/src/map.h @@ -40,6 +40,7 @@ #include "map_selector.h" #include "mapdata.h" #include "maptile_fwd.h" +#include "pathfinding.h" #include "point.h" #include "rng.h" #include "type_id.h" @@ -96,8 +97,6 @@ using VehicleList = std::vector; class map; enum class ter_furn_flag : int; -struct pathfinding_cache; -struct pathfinding_settings; template struct weighted_int_list; struct field_proc_data; @@ -416,11 +415,25 @@ class map void set_outside_cache_dirty( int zlev ); void set_floor_cache_dirty( int zlev ); void set_pathfinding_cache_dirty( int zlev ); - void set_pathfinding_cache_dirty( const tripoint &p ); + void set_pathfinding_cache_dirty( const tripoint_bub_ms &p ); /*@}*/ void invalidate_map_cache( int zlev ); + RealityBubblePathfindingCache *pathfinding_cache() const { + if( !pathfinding_cache_ ) { + pathfinding_cache_ = std::make_unique(); + } + return pathfinding_cache_.get(); + } + + RealityBubblePathfinder *pathfinder() const { + if( !pathfinder_ ) { + pathfinder_ = std::make_unique( pathfinding_cache() ); + } + return pathfinder_.get(); + } + // @returns true if map memory decoration should be re/memorized bool memory_cache_dec_is_dirty( const tripoint &p ) const; // @returns true if map memory terrain should be re/memorized @@ -730,16 +743,23 @@ class map * @param pre_closed Never path through those points. They can still be the source or the destination. */ // TODO: fix point types (remove the first overload) - std::vector route( const tripoint &f, const tripoint &t, - const pathfinding_settings &settings, - const std::function &avoid = []( const tripoint & ) { - return false; - } ) const; + bool can_teleport( const tripoint_bub_ms &t, const PathfindingSettings &settings ) const; + bool can_move( const tripoint_bub_ms &f, const tripoint_bub_ms &t, + const PathfindingSettings &settings ) const; + std::optional move_cost( const tripoint_bub_ms &f, const tripoint_bub_ms &t, + const PathfindingSettings &settings ) const; + std::vector straight_route( const tripoint_bub_ms &f, const tripoint_bub_ms &t, + const PathfindingSettings &settings ) const; + std::vector route( const tripoint_bub_ms &f, const tripoint_bub_ms &t, + const PathfindingSettings &settings ) const; + + // TODO: Update callers to the new PathfindingSettings so that this overload is not necessary. std::vector route( const tripoint_bub_ms &f, const tripoint_bub_ms &t, - const pathfinding_settings &settings, - const std::function &avoid = []( const tripoint & ) { - return false; - } ) const; + const pathfinding_settings &settings ) const; + + // TODO: Replace this with a typesafe variant. + std::vector route( const tripoint &f, const tripoint &t, + const pathfinding_settings &settings ); // Get a straight route from f to t, only along non-rough terrain. Returns an empty vector // if that is not possible. @@ -747,14 +767,17 @@ class map private: // Pathfinding cost helper that computes the cost of moving into |p| from |cur|. // Includes climbing, bashing and opening doors. + // TODO: Combine this logic with the new pathfinding. int cost_to_pass( const tripoint &cur, const tripoint &p, const pathfinding_settings &settings, pf_special p_special ) const; // Pathfinding cost helper that computes the cost of moving into |p| // from |cur| based on perceived danger. // Includes moving through traps. + // TODO: Combine this logic with the new pathfinding. int cost_to_avoid( const tripoint &cur, const tripoint &p, const pathfinding_settings &settings, pf_special p_special ) const; // Sum of cost_to_pass and cost_to_avoid. + // TODO: Combine this logic with the new pathfinding. int extra_cost( const tripoint &cur, const tripoint &p, const pathfinding_settings &settings, pf_special p_special ) const; public: @@ -1149,6 +1172,10 @@ class map return bash_rating( str, tripoint( p, abs_sub.z() ) ); } + // The range of minimum and maximum bash strength needed to bash something on the given tile. + // Returns std::nullopt if there is nothing bashable. + std::optional> bash_range( const tripoint &p, bool allow_floor = false ) const; + // Rubble /** Generates rubble at the given location, if overwrite is true it just writes on top of what currently exists * floor_type is only used if there is a non-bashable wall at the location or with overwrite = true */ @@ -2420,7 +2447,10 @@ class map */ mutable std::array< std::unique_ptr, OVERMAP_LAYERS > caches; - mutable std::array< std::unique_ptr, OVERMAP_LAYERS > pathfinding_caches; + // Pathfinding caches. Lazily initialized, only use via accessors. + mutable std::unique_ptr pathfinding_cache_; + mutable std::unique_ptr pathfinder_; + /** * Set of submaps that contain active items in absolute coordinates. */ @@ -2447,8 +2477,6 @@ class map return caches[zlev + OVERMAP_DEPTH].get(); } - pathfinding_cache &get_pathfinding_cache( int zlev ) const; - visibility_variables visibility_variables_cache; // caches the highest zlevel above which all zlevels are uniform @@ -2470,11 +2498,6 @@ class map return get_cache( zlev ); } - const pathfinding_cache &get_pathfinding_cache_ref( int zlev ) const; - - void update_pathfinding_cache( const tripoint &p ) const; - void update_pathfinding_cache( int zlev ) const; - void update_visibility_cache( int zlev ); void invalidate_visibility_cache(); const visibility_variables &get_visibility_variables_cache() const; diff --git a/src/mapgen.cpp b/src/mapgen.cpp index 904c46f48c958..8a9246735b2e7 100644 --- a/src/mapgen.cpp +++ b/src/mapgen.cpp @@ -6892,6 +6892,7 @@ vehicle *map::add_vehicle( const vproto_id &type, const tripoint &p, const units add_vehicle_to_cache( placed_vehicle ); rebuild_vehicle_level_caches(); + // TODO: Only invalidate points the vehicle is at. set_pathfinding_cache_dirty( p.z ); placed_vehicle->place_zones( *this ); } diff --git a/src/mattack_actors.cpp b/src/mattack_actors.cpp index 9dad99373b36c..921577c5dae59 100644 --- a/src/mattack_actors.cpp +++ b/src/mattack_actors.cpp @@ -144,6 +144,7 @@ bool leap_actor::call( monster &z ) const target.x, target.y, target.z ); std::multimap candidates; + const PathfindingSettings settings = z.get_pathfinding_settings(); for( const tripoint &candidate : here.points_in_radius( z.pos(), max_range ) ) { if( candidate == z.pos() ) { add_msg_debug( debugmode::DF_MATTACK, "Monster at coordinates %d,%d,%d", @@ -166,16 +167,11 @@ bool leap_actor::call( monster &z ) const "Candidate farther from target than optimal path, discarded" ); continue; } - if( !ignore_dest_terrain && !z.will_move_to( candidate ) ) { + if( !ignore_dest_terrain && !z.can_move_to( candidate, settings ) ) { add_msg_debug( debugmode::DF_MATTACK, "Candidate place it can't enter, discarded" ); continue; } - if( !ignore_dest_danger && !z.know_danger_at( candidate ) ) { - add_msg_debug( debugmode::DF_MATTACK, - "Candidate with dangerous conditions, discarded" ); - continue; - } candidates.emplace( candidate_dist, candidate ); } for( const auto &candidate : candidates ) { @@ -590,6 +586,7 @@ int melee_actor::do_grab( monster &z, Creature *target, bodypart_id bp_id ) cons // Drag stuff if( grab_data.drag_distance > 0 ) { int distance = grab_data.drag_distance; + const PathfindingSettings settings = z.get_pathfinding_settings(); while( distance > 0 ) { // Start with the opposite square tripoint opposite_square = z.pos() - ( target->pos() - z.pos() ); @@ -615,7 +612,7 @@ int melee_actor::do_grab( monster &z, Creature *target, bodypart_id bp_id ) cons std::set::iterator intersect_iter = intersect.begin(); std::advance( intersect_iter, rng( 0, intersect.size() - 1 ) ); tripoint target_square = random_entry>( intersect ); - if( z.can_move_to( target_square ) ) { + if( z.can_move_to( target_square, settings ) ) { monster *zz = target->as_monster(); tripoint zpt = z.pos(); z.move_to( target_square, false, false, grab_data.drag_movecost_mod ); diff --git a/src/monattack.cpp b/src/monattack.cpp index b7583bf55823d..603b657364896 100644 --- a/src/monattack.cpp +++ b/src/monattack.cpp @@ -2524,7 +2524,7 @@ bool mattack::formblob( monster *z ) // If we're big enough, spawn a baby blob. shared_ptr_fast mon = make_shared_fast( mon_blob_small ); mon->ammo = mon->type->starting_ammo; - if( mon->will_move_to( dest ) && mon->know_danger_at( dest ) ) { + if( mon->can_move_to( dest ) ) { didit = true; z->set_speed_base( z->get_speed_base() - mon_blob_small->speed ); if( monster *const blob = g->place_critter_around( mon, dest, 0 ) ) { diff --git a/src/monexamine.cpp b/src/monexamine.cpp index eb388327ae594..027307a8c8f50 100644 --- a/src/monexamine.cpp +++ b/src/monexamine.cpp @@ -563,7 +563,7 @@ void insert_battery( monster &z ) bool Character::can_mount( const monster &critter ) const { const auto &avoid = get_path_avoid(); - auto route = get_map().route( pos(), critter.pos(), get_pathfinding_settings(), avoid ); + auto route = get_map().route( pos(), critter.pos(), get_pathfinding_settings() ); if( route.empty() ) { return false; diff --git a/src/monmove.cpp b/src/monmove.cpp index adf1193bbf126..e14335eee7dc7 100644 --- a/src/monmove.cpp +++ b/src/monmove.cpp @@ -128,162 +128,115 @@ bool monster::is_immune_field( const field_type_id &fid ) const return Creature::is_immune_field( fid ); } -static bool z_is_valid( int z ) -{ - return z >= -OVERMAP_DEPTH && z <= OVERMAP_HEIGHT; -} -bool monster::will_move_to( const tripoint &p ) const +PathfindingSettings monster::get_pathfinding_settings( bool avoid_bashing ) const { - map &here = get_map(); - if( here.impassable( p ) ) { - if( digging() ) { - if( !here.has_flag( ter_furn_flag::TFLAG_BURROWABLE, p ) ) { - return false; - } - } else if( !( can_climb() && here.has_flag( ter_furn_flag::TFLAG_CLIMBABLE, p ) ) ) { - return false; - } - } + PathfindingSettings settings = type->path_settings.to_new_pathfinding_settings(); - if( !here.has_vehicle_floor( p ) ) { - if( !can_submerge() && !flies() && here.has_flag( ter_furn_flag::TFLAG_DEEP_WATER, p ) ) { - return false; - } - } + settings.set_size_restriction( get_size() ); - if( digs() && !here.has_flag( ter_furn_flag::TFLAG_DIGGABLE, p ) && - !here.has_flag( ter_furn_flag::TFLAG_BURROWABLE, p ) ) { - return false; - } + settings.set_avoid_bashing( avoid_bashing ); - if( has_flag( mon_flag_AQUATIC ) && ( - !here.has_flag( ter_furn_flag::TFLAG_SWIMMABLE, p ) || - // AQUATIC (confined to water) monster avoid vehicles, unless they are already underneath one - ( here.veh_at( p ) && !here.veh_at( pos() ) ) - ) ) { - return false; - } + settings.set_is_digging( digging() ); - if( has_flag( mon_flag_SUNDEATH ) && g->is_in_sunlight( p ) ) { - return false; - } + settings.set_avoid_climbing( !can_climb() ); - if( get_size() > creature_size::medium && - here.has_flag_ter( ter_furn_flag::TFLAG_SMALL_PASSAGE, p ) ) { - return false; // if a large critter, can't move through tight passages - } + const bool can_fly = flies(); + settings.set_avoid_deep_water( !can_submerge() && !can_fly ); - return true; -} + settings.set_avoid_hard_ground( digs() ); -bool monster::know_danger_at( const tripoint &p ) const -{ - map &here = get_map(); + const bool is_aquatic = has_flag( mon_flag_AQUATIC ); + settings.set_avoid_ground( is_aquatic ); + // AQUATIC (confined to water) monster avoid vehicles, unless they are already underneath one + settings.set_avoid_vehicle( is_aquatic && !get_map().veh_at( pos() ) ); - // Various avoiding behaviors. + // If we hate the sun, stay inside when it is out. + settings.set_avoid_unsheltered( has_flag( mon_flag_SUNDEATH ) && + incident_sun_irradiance( get_weather().weather_id, calendar::turn ) > irradiance::minimal ); + // Various avoiding behaviors. + bool avoid_fire = has_flag( mon_flag_PATH_AVOID_FIRE ); + bool avoid_fall = has_flag( mon_flag_PATH_AVOID_FALL ); bool avoid_simple = has_flag( mon_flag_PATH_AVOID_DANGER ); + bool avoid_sharp = type->path_settings.avoid_sharp; + bool avoid_traps = type->path_settings.avoid_traps; + bool avoid_dangerous_fields = type->path_settings.avoid_dangerous_fields; - bool avoid_fire = avoid_simple || has_flag( mon_flag_PATH_AVOID_FIRE ); - bool avoid_fall = avoid_simple || has_flag( mon_flag_PATH_AVOID_FALL ); - bool avoid_sharp = avoid_simple || get_pathfinding_settings().avoid_sharp; - - bool avoid_dangerous_fields = get_pathfinding_settings().avoid_dangerous_fields; - bool avoid_traps = get_pathfinding_settings().avoid_traps; - - // technically this will shortcut in evaluation from fire or fall - // before hitting simple or complex but this is more explicit - if( avoid_fire || avoid_fall || avoid_simple || - avoid_traps || avoid_dangerous_fields || avoid_sharp ) { - const ter_id target = here.ter( p ); - if( !here.has_vehicle_floor( p ) ) { - // Don't enter lava if we have any concept of heat being bad - if( avoid_fire && target == ter_t_lava ) { - return false; - } + // avoid_simple implies avoid fire, fall, and sharp. + if( avoid_simple ) { + avoid_fire = true; + avoid_fall = true; + avoid_sharp = true; + } - if( avoid_fall ) { - // Don't throw ourselves off cliffs if we have a concept of falling - if( !here.has_floor_or_water( p ) && !flies() ) { - return false; - } + // Don't enter lava if we have any concept of heat being bad + settings.set_avoid_lava( avoid_fire ); - // Don't enter open pits ever unless tiny, can fly or climb well - if( !( type->size == creature_size::tiny || can_climb() ) && - ( target == ter_t_pit || target == ter_t_pit_spiked || target == ter_t_pit_glass ) ) { - return false; - } - } + settings.set_is_flying( can_fly ); + if( can_fly ) { + avoid_fall = false; + } - // Some things are only avoided if we're not attacking - if( get_player_character().get_location() != get_dest() || - attitude( &get_player_character() ) != MATT_ATTACK ) { - // Sharp terrain is ignored while attacking - if( avoid_sharp && here.has_flag( ter_furn_flag::TFLAG_SHARP, p ) && - !( type->size == creature_size::tiny || flies() || - get_armor_type( damage_cut, bodypart_id( "torso" ) ) >= 10 ) ) { - return false; - } - } + // Don't throw ourselves off cliffs if we have a concept of falling + settings.set_avoid_falling( avoid_fall ); - // Don't step on any traps (if we can see) - const trap &target_trap = here.tr_at( p ); - if( avoid_traps && has_flag( mon_flag_SEES ) && - !target_trap.is_benign() && here.has_floor_or_water( p ) ) { - return false; - } - } + // Don't enter open pits ever unless tiny, can fly or climb well + settings.set_avoid_pits( avoid_fall && type->size != creature_size::tiny && !can_climb() ); - const field &target_field = here.field_at( p ); - // Higher awareness is needed for identifying these as threats. - if( avoid_dangerous_fields && is_dangerous_fields( target_field ) ) { - return false; - } + // Some things are only avoided if we're not attacking the player + if( get_player_character().get_location() != get_dest() || + attitude( &get_player_character() ) != MATT_ATTACK ) { + // Sharp terrain is ignored while attacking + settings.set_avoid_sharp( avoid_sharp && !( type->size == creature_size::tiny || can_fly || + get_armor_type( damage_cut, bodypart_id( "torso" ) ) >= 10 ) ); + } else { + // yolo, get that bread, etc + settings.set_avoid_sharp( false ); + } + + // Don't step on any traps (if we can see) + settings.set_avoid_dangerous_traps( avoid_traps && has_flag( mon_flag_SEES ) ); + if( avoid_dangerous_fields ) { + // Don't enter any dangerous fields + settings.set_maybe_avoid_dangerous_fields_fn( [this]( const field_type_id & field_id ) { + return !is_immune_field( field_id ); + } ); + } else { // Without avoid_complex, only fire and electricity are checked for field avoidance. - if( avoid_fire && target_field.find_field( fd_fire ) && !is_immune_field( fd_fire ) ) { - return false; - } - if( avoid_simple && target_field.find_field( fd_electricity ) && - !is_immune_field( fd_electricity ) ) { - return false; + const bool should_avoid_fire = avoid_fire && !is_immune_field( fd_fire ); + const bool should_avoid_simple = avoid_simple && !is_immune_field( fd_electricity ); + if( should_avoid_fire && should_avoid_simple ) { + settings.set_maybe_avoid_dangerous_fields_fn( []( const field_type_id & field_id ) { + return field_id == fd_fire || field_id == fd_electricity; + } ); + } else if( should_avoid_fire ) { + settings.set_maybe_avoid_dangerous_fields_fn( []( const field_type_id & field_id ) { + return field_id == fd_fire; + } ); + } else if( should_avoid_simple ) { + settings.set_maybe_avoid_dangerous_fields_fn( []( const field_type_id & field_id ) { + return field_id == fd_electricity; + } ); + } else { + settings.set_maybe_avoid_dangerous_fields_fn(); } } - - return true; + return settings; } -bool monster::can_reach_to( const tripoint &p ) const +bool monster::can_move_to( const tripoint &p, const PathfindingSettings &settings ) const { - map &here = get_map(); - if( p.z > pos().z && z_is_valid( pos().z ) ) { - if( here.has_flag( ter_furn_flag::TFLAG_RAMP_UP, tripoint( p.xy(), p.z - 1 ) ) ) { - return true; - } - if( !here.has_flag( ter_furn_flag::TFLAG_GOES_UP, pos() ) && here.has_floor( p ) ) { - // can't go through the roof - return false; - } - } else if( p.z < pos().z && z_is_valid( pos().z ) ) { - const tripoint above( p.xy(), p.z + 1 ); - if( here.has_flag( ter_furn_flag::TFLAG_RAMP_DOWN, above ) ) { - return true; - } - if( !here.has_flag( ter_furn_flag::TFLAG_GOES_DOWN, pos() ) && - ( here.has_floor( above ) || ( !flies() && !here.has_floor_or_water( above ) ) ) ) { - // can't go through the floor - // Check floors for flying monsters movement - return false; - } + const map &here = get_map(); + const tripoint_bub_ms &from = pos_bub(); + if( here.inbounds( from ) ) { + return here.can_move( from, tripoint_bub_ms( p ), settings ); + } else { + return here.can_teleport( tripoint_bub_ms( p ), settings ); } - return true; } -bool monster::can_move_to( const tripoint &p ) const -{ - return can_reach_to( p ) && will_move_to( p ); -} float monster::rate_target( Creature &c, float best, bool smart ) const { @@ -961,11 +914,12 @@ void monster::move() bool moved = false; tripoint destination; + PathfindingSettings settings = get_pathfinding_settings(); bool try_to_move = false; creature_tracker &creatures = get_creature_tracker(); for( const tripoint &dest : here.points_in_radius( pos(), 1 ) ) { if( dest != pos() ) { - if( can_move_to( dest ) && + if( can_move_to( dest, settings ) && creatures.creature_at( dest, true ) == nullptr ) { try_to_move = true; break; @@ -1004,26 +958,30 @@ void monster::move() path.erase( path.begin() ); } - const pathfinding_settings &pf_settings = get_pathfinding_settings(); - if( pf_settings.max_dist >= rl_dist( get_location(), get_dest() ) && + if( settings.max_distance() >= rl_dist( get_location(), get_dest() ) && ( path.empty() || rl_dist( pos(), path.front() ) >= 2 || path.back() != local_dest ) ) { // We need a new path + path.clear(); + // Temporarily allow bashing to find a path through bashable terrain. + settings.set_avoid_bashing( false ); + if( can_pathfind() ) { - path = here.route( pos(), local_dest, pf_settings, get_path_avoid() ); + for( const tripoint_bub_ms &p : here.route( pos_bub(), tripoint_bub_ms( local_dest ), settings ) ) { + path.push_back( p.raw() ); + } if( path.empty() ) { increment_pathfinding_cd(); } } else { - path = here.straight_route( pos(), local_dest ); - if( !path.empty() ) { - if( std::any_of( path.begin(), path.end(), get_path_avoid() ) ) { - path.clear(); - } + for( const tripoint_bub_ms &p : here.straight_route( pos_bub(), tripoint_bub_ms( local_dest ), + settings ) ) { + path.push_back( p.raw() ); } } if( !path.empty() ) { reset_pathfinding_cd(); } + settings.set_avoid_bashing( true ); } // Try to respect old paths, even if we can't pathfind at the moment @@ -1106,7 +1064,7 @@ void monster::move() } const tripoint_abs_ms candidate_abs = get_map().getglobal( candidate ); - if( candidate.z != posz() ) { + if( candidate.z != pos().z ) { bool can_z_move = true; if( !here.valid_move( pos(), candidate, false, true, via_ramp ) ) { // Can't phase through floor @@ -1115,10 +1073,10 @@ void monster::move() // If we're trying to go up but can't fly, check if we can climb. If we can't, then don't // This prevents non-climb/fly enemies running up walls - if( candidate.z > posz() && !( via_ramp || flies() ) ) { + if( candidate.z > pos().z && !( via_ramp || flies() ) ) { if( !can_climb() || !here.has_floor_or_support( candidate ) ) { - if( ( !here.has_flag( ter_furn_flag::TFLAG_SWIMMABLE, pos() ) || - !here.has_flag( ter_furn_flag::TFLAG_SWIMMABLE, candidate ) ) ) { + if( !here.has_flag( ter_furn_flag::TFLAG_SWIMMABLE, pos() ) || + !here.has_flag( ter_furn_flag::TFLAG_SWIMMABLE, candidate ) ) { // Can't "jump" up a whole z-level can_z_move = false; } @@ -1130,8 +1088,8 @@ void monster::move() if( !can_z_move && posx() / ( SEEX * 2 ) == candidate.x / ( SEEX * 2 ) && posy() / ( SEEY * 2 ) == candidate.y / ( SEEY * 2 ) ) { - const tripoint upper = candidate.z > posz() ? candidate : pos(); - const tripoint lower = candidate.z > posz() ? pos() : candidate; + const tripoint upper = candidate.z > pos().z ? candidate : pos(); + const tripoint lower = candidate.z > pos().z ? pos() : candidate; if( here.has_flag( ter_furn_flag::TFLAG_GOES_DOWN, upper ) && here.has_flag( ter_furn_flag::TFLAG_GOES_UP, lower ) ) { can_z_move = true; @@ -1184,7 +1142,7 @@ void monster::move() // Try to shove vehicle out of the way shove_vehicle( destination, candidate ); // Bail out if we can't move there and we can't bash. - if( !pathed && !can_move_to( candidate ) ) { + if( !pathed && !can_move_to( candidate, settings ) ) { if( !can_bash ) { continue; } @@ -1244,6 +1202,14 @@ void monster::move() } else if( drag_to != get_location() && creatures.creature_at( drag_to ) == nullptr ) { dragged_foe->move_to( drag_to ); } + + // If we ended up in a place we didn't plan to, maybe we stumbled or fell, either way the path is + // invalid now. + const tripoint new_pos = pos(); + if( !path.empty() && path.front() != new_pos && new_pos != pos() ) { + path.clear(); + } + } } else { moves = 0; @@ -1486,11 +1452,12 @@ tripoint monster::scent_move() } } + const PathfindingSettings settings = get_pathfinding_settings(); for( const tripoint &direction : sdirection ) { // Add some randomness to make creatures zigzag towards the source for( const tripoint &dest : here.points_in_radius( direction, 1 ) ) { if( here.valid_move( pos(), dest, can_bash, true ) && - ( can_move_to( dest ) || ( dest == player_character.pos() ) || + ( can_move_to( dest, settings ) || ( dest == player_character.pos() ) || ( can_bash && here.bash_rating( bash_estimate(), dest ) > 0 ) ) ) { smoves.push_back( dest ); } @@ -2192,10 +2159,11 @@ void monster::stumble() valid_stumbles.push_back( below ); } + const PathfindingSettings settings = get_pathfinding_settings(); creature_tracker &creatures = get_creature_tracker(); while( !valid_stumbles.empty() && !is_dead() ) { const tripoint dest = random_entry_removed( valid_stumbles ); - if( can_move_to( dest ) && + if( can_move_to( dest, settings ) && //Stop zombies and other non-breathing monsters wandering INTO water //(Unless they can swim/are aquatic) //But let them wander OUT of water if they are there. @@ -2285,76 +2253,6 @@ void monster::knock_back_to( const tripoint &to ) check_dead_state(); } -/* will_reach() is used for determining whether we'll get to stairs (and - * potentially other locations of interest). It is generally permissive. - * TODO: Pathfinding; - Make sure that non-smashing monsters won't "teleport" through windows - Injure monsters if they're gonna be walking through pits or whatever - */ -bool monster::will_reach( const point &p ) -{ - monster_attitude att = attitude( &get_player_character() ); - if( att != MATT_FOLLOW && att != MATT_ATTACK && att != MATT_FRIEND ) { - return false; - } - - if( digs() || has_flag( mon_flag_AQUATIC ) ) { - return false; - } - - if( ( has_flag( mon_flag_IMMOBILE ) || has_flag( mon_flag_RIDEABLE_MECH ) ) && - ( pos().xy() != p ) ) { - return false; - } - - auto path = get_map().route( pos(), tripoint( p, posz() ), get_pathfinding_settings() ); - if( path.empty() ) { - return false; - } - - if( has_flag( mon_flag_SMELLS ) && get_scent().get( pos() ) > 0 && - get_scent().get( { p, posz() } ) > get_scent().get( pos() ) ) { - return true; - } - - if( can_hear() && wandf > 0 && rl_dist( get_map().getlocal( wander_pos ).xy(), p ) <= 2 && - rl_dist( get_location().xy(), wander_pos.xy() ) <= wandf ) { - return true; - } - - if( can_see() && sees( tripoint( p, posz() ) ) ) { - return true; - } - - return false; -} - -int monster::turns_to_reach( const point &p ) -{ - map &here = get_map(); - // HACK: This function is a(n old) temporary hack that should soon be removed - auto path = here.route( pos(), tripoint( p, posz() ), get_pathfinding_settings() ); - if( path.empty() ) { - return 999; - } - - double turns = 0.; - for( size_t i = 0; i < path.size(); i++ ) { - const tripoint &next = path[i]; - if( here.impassable( next ) ) { - // No bashing through, it looks stupid when you go back and find - // the doors intact. - return 999; - } else if( i == 0 ) { - turns += static_cast( calc_movecost( pos(), next ) ) / get_speed(); - } else { - turns += static_cast( calc_movecost( path[i - 1], next ) ) / get_speed(); - } - } - - return static_cast( turns + .9 ); // Halve (to get turns) and round up -} - void monster::shove_vehicle( const tripoint &remote_destination, const tripoint &nearby_destination ) { diff --git a/src/monster.cpp b/src/monster.cpp index 9b43213341c88..b151562649749 100644 --- a/src/monster.cpp +++ b/src/monster.cpp @@ -1343,8 +1343,8 @@ bool monster::has_intelligence() const has_flag( mon_flag_PATH_AVOID_FIRE ) || has_flag( mon_flag_PATH_AVOID_DANGER ) || has_flag( mon_flag_PRIORITIZE_TARGETS ) || - get_pathfinding_settings().avoid_sharp || - get_pathfinding_settings().avoid_traps; + type->path_settings.avoid_sharp || + type->path_settings.avoid_traps; } std::vector monster::get_absorb_material() const @@ -1384,7 +1384,7 @@ tripoint_abs_ms monster::get_dest() const void monster::set_dest( const tripoint_abs_ms &p ) { - if( !goal || rl_dist( p, *goal ) > 2 ) { + if( !goal || rl_dist( p, *goal ) > 4 ) { reset_pathfinding_cd(); } goal = p; @@ -3943,36 +3943,6 @@ void monster::on_load() name(), to_turns( dt ), healed, healed_speed ); } -const pathfinding_settings &monster::get_pathfinding_settings() const -{ - return type->path_settings; -} - -std::function monster::get_path_avoid() const -{ - return [this]( const tripoint & p ) { - map &here = get_map(); - // If we can't move there and can't bash it, don't path through it. - if( !can_move_to( p ) && ( bash_skill() <= 0 || !here.is_bashable( p ) ) ) { - return true; - } - - // Avoid nearby creatures if we have the flag. - int radius; - if( has_flag( mon_flag_PRIORITIZE_TARGETS ) ) { - radius = 2; - } else if( has_flag( mon_flag_PATH_AVOID_DANGER ) ) { - radius = 1; - } else { - return false; - } - if( rl_dist( p, pos() ) <= radius && get_creature_tracker().creature_at( p ) ) { - return true; - } - return false; - }; -} - double monster::calculate_by_enchantment( double modify, enchant_vals::mod value, bool round_output ) const { diff --git a/src/monster.h b/src/monster.h index fe096e1577c02..61add08f5d237 100644 --- a/src/monster.h +++ b/src/monster.h @@ -15,12 +15,14 @@ #include #include "calendar.h" +#include "cata_utility.h" #include "character_id.h" #include "color.h" #include "compatibility.h" #include "creature.h" #include "damage.h" #include "enums.h" +#include "pathfinding.h" #include "point.h" #include "type_id.h" #include "units_fwd.h" @@ -39,7 +41,6 @@ namespace catacurses class window; } // namespace catacurses struct dealt_projectile_attack; -struct pathfinding_settings; struct trap; enum class mon_trigger : int; @@ -204,18 +205,17 @@ class monster : public Creature * * This is used in pathfinding and ONLY checks the terrain. It ignores players * and monsters, which might only block this tile temporarily. - * will_move_to() checks for impassable terrain etc - * can_reach_to() checks for z-level difference. - * can_move_to() is a wrapper for both of them. - * know_danger_at() checks for fire, trap etc. (flag PATH_AVOID_) + * + * If called multiple times in a loop, prefer getting the settings separately and + * reusing them. */ - bool can_move_to( const tripoint &p ) const; - bool can_reach_to( const tripoint &p ) const; - bool will_move_to( const tripoint &p ) const; - bool know_danger_at( const tripoint &p ) const; + bool can_move_to( const tripoint &p ) const { + return can_move_to( p, get_pathfinding_settings() ); + } + bool can_move_to( const tripoint &p, const PathfindingSettings &settings ) const; - bool will_reach( const point &p ); // Do we have plans to get to (x, y)? - int turns_to_reach( const point &p ); // How long will it take? + // Get the pathfinding settings for this monster. + PathfindingSettings get_pathfinding_settings( bool avoid_bashing = true ) const; // Returns true if the monster has a current goal bool has_dest() const; @@ -603,8 +603,6 @@ class monster : public Creature */ void on_load(); - const pathfinding_settings &get_pathfinding_settings() const override; - std::function get_path_avoid() const override; double calculate_by_enchantment( double modify, enchant_vals::mod value, bool round_output = false ) const; private: diff --git a/src/npcmove.cpp b/src/npcmove.cpp index eb00d020961d7..46761aa3c3c4c 100644 --- a/src/npcmove.cpp +++ b/src/npcmove.cpp @@ -2828,8 +2828,8 @@ bool npc::update_path( const tripoint &p, const bool no_bashing, bool force ) } } - std::vector new_path = get_map().route( pos(), p, get_pathfinding_settings( no_bashing ), - get_path_avoid() ); + std::vector new_path = get_map().route( pos(), p, + get_pathfinding_settings( no_bashing ) ); if( new_path.empty() ) { if( !ai_cache.sound_alerts.empty() ) { ai_cache.sound_alerts.erase( ai_cache.sound_alerts.begin() ); @@ -4993,7 +4993,7 @@ void npc::go_to_omt_destination() } } } - path = here.route( pos(), centre_sub, get_pathfinding_settings(), get_path_avoid() ); + path = here.route( pos(), centre_sub, get_pathfinding_settings() ); add_msg_debug( debugmode::DF_NPC, "%s going %s->%s", get_name(), omt_pos.to_string_writable(), goal.to_string_writable() ); diff --git a/src/npctrade_utils.cpp b/src/npctrade_utils.cpp index 98eb063fea8c0..2194646a30db3 100644 --- a/src/npctrade_utils.cpp +++ b/src/npctrade_utils.cpp @@ -94,8 +94,7 @@ void add_fallback_zone( npc &guy ) ( here.furn( t_here )->max_volume > ter_t_floor->max_volume || here.furn( t_here )->has_flag( ter_furn_flag::TFLAG_CONTAINER ) ) && here.can_put_items_ter_furn( t_here ) && - !here.route( guy.pos_bub(), t_here, guy.get_pathfinding_settings(), - guy.get_path_avoid() ) + !here.route( guy.pos_bub(), t_here, guy.get_pathfinding_settings() ) .empty() ) { points.emplace_back( t ); } diff --git a/src/pathfinding.cpp b/src/pathfinding.cpp index 72dfc31d3c4ec..536d7e99e6941 100644 --- a/src/pathfinding.cpp +++ b/src/pathfinding.cpp @@ -27,592 +27,736 @@ #include "vehicle.h" #include "vpart_position.h" -// Turns two indexed to a 2D array into an index to equivalent 1D array -static constexpr int flat_index( const point &p ) +static const ter_id ter_t_pit = ter_id( "t_pit" ); +static const ter_id ter_t_pit_spiked = ter_id( "t_pit_spiked" ); +static const ter_id ter_t_pit_glass = ter_id( "t_pit_glass" ); +static const ter_id ter_t_lava = ter_id( "t_lava" ); + +RealityBubblePathfindingCache::RealityBubblePathfindingCache() { - return ( p.x * MAPSIZE_Y ) + p.y; + for( int z = -OVERMAP_DEPTH; z <= OVERMAP_HEIGHT; ++z ) { + dirty_z_levels_.emplace( z ); + } } -// Flattened 2D array representing a single z-level worth of pathfinding data -struct path_data_layer { - // Closed/open is accessed way more often than all other values here - std::bitset< MAPSIZE_X *MAPSIZE_Y > closed; - std::bitset< MAPSIZE_X *MAPSIZE_Y > open; - std::array< int, MAPSIZE_X *MAPSIZE_Y > score; - std::array< int, MAPSIZE_X *MAPSIZE_Y > gscore; - std::array< tripoint, MAPSIZE_X *MAPSIZE_Y > parent; - - void reset() { - closed.reset(); - open.reset(); - } -}; - -struct pathfinder { - using queue_type = - std::priority_queue< std::pair, std::vector< std::pair >, pair_greater_cmp_first >; - queue_type open; - std::array< std::unique_ptr< path_data_layer >, OVERMAP_LAYERS > path_data; - - path_data_layer &get_layer( const int z ) { - std::unique_ptr< path_data_layer > &ptr = path_data[z + OVERMAP_DEPTH]; - if( ptr != nullptr ) { - return *ptr; - } - ptr = std::make_unique(); - return *ptr; - } - - void reset( int minz, int maxz ) { - for( int i = minz; i <= maxz; ++i ) { - std::unique_ptr< path_data_layer > &ptr = path_data[i + OVERMAP_DEPTH]; - if( ptr != nullptr ) { - path_data[i + OVERMAP_DEPTH]->reset(); - } +// Modifies `t` to point to a tile with `flag` in a 1-submap radius of `t`'s original value, searching +// nearest points first (starting with `t` itself). +// Returns false if it could not find a suitable point +bool RealityBubblePathfindingCache::vertical_move_destination( const map &here, ter_furn_flag flag, + tripoint &t ) const +{ + const int z = t.z; + if( const std::optional p = find_point_closest_first( t.xy(), 0, SEEX, [&here, flag, + z]( const point & p ) { + if( p.x >= 0 && p.x < MAPSIZE_X && p.y >= 0 && p.y < MAPSIZE_Y ) { + const tripoint t2( p, z ); + return here.has_flag( flag, t2 ); } - open = queue_type(); + return false; + } ) ) { + t = tripoint( *p, z ); + return true; } + return false; +} - bool empty() const { - return open.empty(); +void RealityBubblePathfindingCache::update( const map &here, int min_z, int max_z ) +{ + if( dirty_z_levels_.empty() && dirty_positions_.empty() ) { + return; } - tripoint get_next() { - const auto pt = open.top(); - open.pop(); - return pt.second; - } + cata_assert( min_z <= max_z ); - void add_point( const int gscore, const int score, const tripoint &from, const tripoint &to ) { - path_data_layer &layer = get_layer( to.z ); - const int index = flat_index( to.xy() ); - if( layer.closed[index] ) { - return; + const int size = here.getmapsize(); + for( const int z : dirty_z_levels_ ) { + if( z < min_z || z > max_z || !here.inbounds_z( z ) ) { + continue; } - if( layer.open[index] && gscore >= layer.gscore[index] ) { - return; + for( int y = 0; y < size * SEEY; ++y ) { + for( int x = 0; x < size * SEEX; ++x ) { + const tripoint_bub_ms p( x, y, z ); + invalidate_dependants( p ); + update( here, p ); + } } - - layer.open[index] = true; - layer.gscore[index] = gscore; - layer.parent[index] = from; - layer.score [index] = score; - open.emplace( score, to ); - } - - void close_point( const tripoint &p ) { - path_data_layer &layer = get_layer( p.z ); - const int index = flat_index( p.xy() ); - layer.closed[index] = true; } - - void unclose_point( const tripoint &p ) { - path_data_layer &layer = get_layer( p.z ); - const int index = flat_index( p.xy() ); - layer.closed[index] = false; + for( const int z : dirty_z_levels_ ) { + if( min_z <= z && z <= max_z ) { + dirty_positions_.erase( z ); + } } -}; -static pathfinder pf; - -// Modifies `t` to point to a tile with `flag` in a 1-submap radius of `t`'s original value, -// searching nearest points first (starting with `t` itself). -// return false if it could not find a suitable point -static bool vertical_move_destination( const map &m, ter_furn_flag flag, tripoint &t ) -{ - const pathfinding_cache &pf_cache = m.get_pathfinding_cache_ref( t.z ); - for( const point &p : closest_points_first( t.xy(), SEEX ) ) { - if( pf_cache.special[p.x][p.y] & PF_UPDOWN ) { - const tripoint t2( p, t.z ); - if( m.has_flag( flag, t2 ) ) { - t = t2; - return true; + for( const auto& [z, dirty_points] : dirty_positions_ ) { + if( z < min_z || z > max_z || !here.inbounds_z( z ) ) { + continue; + } + for( const point_bub_ms &p : dirty_points ) { + tripoint_bub_ms t( p, z ); + if( here.inbounds( t ) ) { + update( here, t ); } } } - return false; + + for( int i = min_z; i <= max_z; ++i ) { + dirty_z_levels_.erase( i ); + dirty_positions_.erase( i ); + } } -template -static bool is_disjoint( const Set1 &set1, const Set2 &set2 ) +void RealityBubblePathfindingCache::update( const map &here, const tripoint_bub_ms &p ) { - if( set1.empty() || set2.empty() ) { - return true; - } + PathfindingFlags flags; - typename Set1::const_iterator it1 = set1.begin(); - typename Set1::const_iterator it1_end = set1.end(); + const const_maptile &tile = here.maptile_at( p ); + const ter_t &terrain = tile.get_ter_t(); + const ter_id terrain_id = tile.get_ter(); + const furn_t &furniture = tile.get_furn_t(); + const optional_vpart_position veh = here.veh_at( p ); - typename Set2::const_iterator it2 = set2.begin(); - typename Set2::const_iterator it2_end = set2.end(); + const int orig_cost = here.move_cost( p ); + const int cost = orig_cost < 0 ? 0 : orig_cost; - if( *set2.rbegin() < *it1 || *set1.rbegin() < *it2 ) { - return true; + if( cost > 2 ) { + flags |= PathfindingFlag::Slow; } - while( it1 != it1_end && it2 != it2_end ) { - if( *it1 == *it2 ) { - return false; + if( !terrain.has_flag( ter_furn_flag::TFLAG_BURROWABLE ) && + !terrain.has_flag( ter_furn_flag::TFLAG_DIGGABLE ) ) { + flags |= PathfindingFlag::HardGround; + } + + if( veh ) { + flags |= PathfindingFlag::Vehicle; + } + + bool try_to_bash = false; + bool impassable = flags.is_set( PathfindingFlag::HardGround ); + if( cost == 0 ) { + flags |= PathfindingFlag::Obstacle; + try_to_bash = true; + if( terrain.open || furniture.open ) { + impassable = false; + flags |= PathfindingFlag::Door; + + if( terrain.has_flag( ter_furn_flag::TFLAG_OPENCLOSE_INSIDE ) || + furniture.has_flag( ter_furn_flag::TFLAG_OPENCLOSE_INSIDE ) ) { + flags |= PathfindingFlag::InsideDoor; + } } - if( *it1 < *it2 ) { - it1++; - } else { - it2++; + + if( veh ) { + if( const auto vpobst = veh->obstacle_at_part() ) { + const int vpobst_i = vpobst->part_index(); + const vehicle &v = veh->vehicle(); + const int open_inside = v.next_part_to_open( vpobst_i, false ); + if( open_inside != -1 ) { + impassable = false; + flags |= PathfindingFlag::Door; + + const int open_outside = v.next_part_to_open( vpobst_i, true ); + if( open_inside != open_outside ) { + flags |= PathfindingFlag::InsideDoor; + } + const int lock = v.next_part_to_unlock( vpobst_i, false ); + if( lock != -1 ) { + flags |= PathfindingFlag::LockedDoor; + } + } + } } - } - return true; -} + if( terrain.has_flag( ter_furn_flag::TFLAG_CLIMBABLE ) ) { + impassable = false; + flags |= PathfindingFlag::Climbable; + } + } else { + impassable = false; + // Size restrictions only apply to otherwise passable tiles. + if( terrain.has_flag( ter_furn_flag::TFLAG_SMALL_PASSAGE ) ) { + flags |= PathfindingFlag::RestrictLarge | PathfindingFlag::RestrictHuge; + } + if( veh ) { + if( auto cargo = veh->cargo(); cargo && !cargo->has_feature( "CARGO_PASSABLE" ) ) { + const units::volume free_volume = cargo->items().free_volume(); + if( free_volume < 11719_ml ) { + flags |= PathfindingFlag::RestrictTiny; + } + if( free_volume < 23438_ml ) { + flags |= PathfindingFlag::RestrictSmall; + } + if( free_volume < 46875_ml ) { + flags |= PathfindingFlag::RestrictMedium; + } + if( free_volume < 93750_ml ) { + flags |= PathfindingFlag::RestrictLarge; + } + if( free_volume < 187500_ml ) { + flags |= PathfindingFlag::RestrictHuge; + } + } + } -std::vector map::straight_route( const tripoint &f, const tripoint &t ) const -{ - std::vector ret; - if( f == t || !inbounds( f ) ) { - return ret; - } - if( !inbounds( t ) ) { - tripoint clipped = t; - clip_to_bounds( clipped ); - return straight_route( f, clipped ); - } - if( f.z == t.z ) { - ret = line_to( f, t ); - const pathfinding_cache &pf_cache = get_pathfinding_cache_ref( f.z ); - // Check all points for any special case (including just hard terrain) - if( std::any_of( ret.begin(), ret.end(), [&pf_cache]( const tripoint & p ) { - constexpr pf_special non_normal = PF_SLOW | PF_WALL | PF_VEHICLE | PF_TRAP | PF_SHARP; - return pf_cache.special[p.x][p.y] & non_normal; - } ) ) { - ret.clear(); + if( flags & PathfindingSettings::AnySizeRestriction ) { + try_to_bash = true; } } - return ret; -} -static constexpr int PF_IMPASSABLE = -1; -static constexpr int PF_IMPASSABLE_FROM_HERE = -2; -int map::cost_to_pass( const tripoint &cur, const tripoint &p, const pathfinding_settings &settings, - pf_special p_special ) const -{ - constexpr pf_special non_normal = PF_SLOW | PF_WALL | PF_VEHICLE | PF_TRAP | PF_SHARP; - if( !( p_special & non_normal ) ) { - // Boring flat dirt - the most common case above the ground - return 2; + if( try_to_bash && here.is_bashable( p.raw() ) ) { + if( const auto bash_range = here.bash_range( p.raw() ) ) { + flags |= PathfindingFlag::Bashable; + impassable = false; + bash_range_ref( p ) = *bash_range; + } } - if( settings.avoid_rough_terrain ) { - return PF_IMPASSABLE; + if( impassable ) { + flags |= PathfindingFlag::Impassable; } - if( settings.avoid_sharp && ( p_special & PF_SHARP ) ) { - return PF_IMPASSABLE; + if( cost > std::numeric_limits::max() ) { + debugmsg( "Tile move cost too large for cache: %s, %d", terrain_id.id().str(), cost ); + } else { + move_cost_ref( p ) = cost < 2 ? 2 : cost; } - const int bash = settings.bash_strength; - const bool allow_open_doors = settings.allow_open_doors; - const bool allow_unlock_doors = settings.allow_unlock_doors; - const int climb_cost = settings.climb_cost; + if( terrain.has_flag( ter_furn_flag::TFLAG_NO_FLOOR ) ) { + flags |= PathfindingFlag::Air; + } else if( terrain.has_flag( ter_furn_flag::TFLAG_SWIMMABLE ) ) { + flags |= PathfindingFlag::Swimmable; + } else { + flags |= PathfindingFlag::Ground; + } - int part = -1; - const const_maptile &tile = maptile_at_internal( p ); - const ter_t &terrain = tile.get_ter_t(); - const furn_t &furniture = tile.get_furn_t(); - const field &field = tile.get_field(); - const vehicle *veh = veh_at_internal( p, part ); - - const int cost = move_cost_internal( furniture, terrain, field, veh, part ); - - // If we can just walk into the tile, great. That's the cost. - if( cost != 0 ) { - return cost; - } - - // Otherwise we'll consider climbing, opening doors, and bashing, in that order. - // Should match logic in monster::move_to and npc::move_to. - - // If there's a vehicle here, we need to assess that instead of the terrain. - if( veh != nullptr ) { - const auto vpobst = vpart_position( const_cast( *veh ), part ).obstacle_at_part(); - part = vpobst ? vpobst->part_index() : -1; - int dummy = -1; - const bool is_outside_veh = veh_at_internal( cur, dummy ) != veh; - - if( part >= 0 ) { - if( allow_open_doors && veh->next_part_to_open( part, is_outside_veh ) != -1 ) { - // Handle car doors, but don't try to path through curtains - return 10; // One turn to open, 4 to move there - } else if( allow_unlock_doors && veh->next_part_to_unlock( part, is_outside_veh ) != -1 ) { - return 12; // 2 turns to open, 4 to move there - } else if( bash > 0 ) { - // Car obstacle that isn't a door - // TODO: Account for armor - int hp = veh->part( part ).hp(); - if( hp / 20 > bash ) { - // Threshold damage thing means we just can't bash this down - return PF_IMPASSABLE; - } else if( hp / 10 > bash ) { - // Threshold damage thing means we will fail to deal damage pretty often - hp *= 2; - } + const bool has_vehicle_floor = here.has_vehicle_floor( p ); + if( !has_vehicle_floor ) { + if( terrain_id == ter_t_pit || terrain_id == ter_t_pit_spiked || terrain_id == ter_t_pit_glass ) { + flags |= PathfindingFlag::Pit; + } - return 2 * hp / bash + 8 + 4; - } else { - const vehicle_part &vp = veh->part( part ); - if( allow_open_doors && vp.info().has_flag( VPFLAG_OPENABLE ) ) { - // If we can open doors in general but weren't - // able to open this one, we might be able to - // open it if we try from another direction. - return PF_IMPASSABLE_FROM_HERE; - } else { - // Won't be openable, don't try from other sides - return PF_IMPASSABLE; - } - } + if( terrain_id == ter_t_lava ) { + flags |= PathfindingFlag::Lava; + } + + if( terrain.has_flag( ter_furn_flag::TFLAG_DEEP_WATER ) ) { + flags |= PathfindingFlag::DeepWater; + } + + if( !tile.get_trap_t().is_benign() ) { + flags |= PathfindingFlag::DangerousTrap; + } + + if( terrain.has_flag( ter_furn_flag::TFLAG_SHARP ) ) { + flags |= PathfindingFlag::Sharp; } } - // If we can climb it, great! - if( climb_cost > 0 && p_special & PF_CLIMBABLE ) { - return climb_cost; + if( terrain.has_flag( ter_furn_flag::TFLAG_BURROWABLE ) ) { + flags |= PathfindingFlag::Burrowable; } - // If it's a door and we can open it from the tile we're on, cool. - if( allow_open_doors && ( terrain.open || furniture.open ) && - ( ( !terrain.has_flag( ter_furn_flag::TFLAG_OPENCLOSE_INSIDE ) && - !furniture.has_flag( ter_furn_flag::TFLAG_OPENCLOSE_INSIDE ) ) || - !is_outside( cur ) ) ) { - // Only try to open INSIDE doors from the inside - // To open and then move onto the tile - return 4; + if( !g->is_sheltered( p.raw() ) ) { + flags |= PathfindingFlag::Unsheltered; } - // Otherwise, if we can bash, we'll consider that. - if( bash > 0 ) { - const int rating = bash_rating_internal( bash, furniture, terrain, false, veh, part ); + for( const auto &fld : tile.get_field() ) { + const field_entry &cur = fld.second; + if( cur.is_dangerous() ) { + flags |= PathfindingFlag::DangerousField; + break; + } + } - if( rating > 1 ) { - // Expected number of turns to bash it down, 1 turn to move there - // and 5 turns of penalty not to trash everything just because we can - return ( 20 / rating ) + 2 + 10; + if( p.z() < OVERMAP_HEIGHT ) { + up_destinations_.erase( p ); + const tripoint_bub_ms up( p.xy(), p.z() + 1 ); + if( terrain.has_flag( ter_furn_flag::TFLAG_GOES_UP ) ) { + if( std::optional dest = g->find_stairs( here, up.z(), p.raw() ) ) { + if( vertical_move_destination( here, ter_furn_flag::TFLAG_GOES_DOWN, *dest ) ) { + tripoint_bub_ms d( *dest ); + flags |= PathfindingFlag::GoesUp; + up_destinations_.emplace( p, d ); + dependants_by_position_[d].push_back( p ); + } + } else { + dependants_by_position_[up].push_back( p ); + } } - if( rating == 1 ) { - // Desperate measures, avoid whenever possible - return 500; + if( terrain.has_flag( ter_furn_flag::TFLAG_RAMP ) || + terrain.has_flag( ter_furn_flag::TFLAG_RAMP_UP ) ) { + dependants_by_position_[up].push_back( p ); + if( here.valid_move( p.raw(), up.raw(), false, true, true ) ) { + flags |= PathfindingFlag::RampUp; + } } } - // If we can open doors generally but couldn't open this one, maybe we can - // try from another direction. - if( allow_open_doors && terrain.open && furniture.open ) { - return PF_IMPASSABLE_FROM_HERE; + if( p.z() > -OVERMAP_DEPTH ) { + down_destinations_.erase( p ); + const tripoint_bub_ms down( p.xy(), p.z() - 1 ); + if( terrain.has_flag( ter_furn_flag::TFLAG_GOES_DOWN ) ) { + if( std::optional dest = g->find_stairs( here, down.z(), p.raw() ) ) { + if( vertical_move_destination( here, ter_furn_flag::TFLAG_GOES_UP, *dest ) ) { + tripoint_bub_ms d( *dest ); + flags |= PathfindingFlag::GoesDown; + down_destinations_.emplace( p, d ); + dependants_by_position_[d].push_back( p ); + } + } else { + dependants_by_position_[down].push_back( p ); + } + } + + if( terrain.has_flag( ter_furn_flag::TFLAG_RAMP_DOWN ) ) { + dependants_by_position_[down].push_back( p ); + if( here.valid_move( p.raw(), down.raw(), false, true, true ) ) { + flags |= PathfindingFlag::RampDown; + } + } } - return PF_IMPASSABLE; + flags_ref( p ) = flags; } -int map::cost_to_avoid( const tripoint & /*cur*/, const tripoint &p, - const pathfinding_settings &settings, pf_special p_special ) const +void PathfindingSettings::set_size_restriction( std::optional size_restriction ) { - if( settings.avoid_traps && ( p_special & PF_TRAP ) ) { - const const_maptile &tile = maptile_at_internal( p ); - const ter_t &terrain = tile.get_ter_t(); - const trap &ter_trp = terrain.trap.obj(); - const trap &trp = ter_trp.is_benign() ? tile.get_trap_t() : ter_trp; - - // NO_FLOOR is a special case handled in map::route - if( !trp.is_benign() && !terrain.has_flag( ter_furn_flag::TFLAG_NO_FLOOR ) ) { - return 500; + size_restriction_mask_.clear(); + if( size_restriction ) { + switch( *size_restriction ) { + case creature_size::tiny: + size_restriction_mask_ = PathfindingFlag::RestrictTiny; + break; + case creature_size::small: + size_restriction_mask_ = PathfindingFlag::RestrictSmall; + break; + case creature_size::medium: + size_restriction_mask_ = PathfindingFlag::RestrictMedium; + break; + case creature_size::large: + size_restriction_mask_ = PathfindingFlag::RestrictLarge; + break; + case creature_size::huge: + size_restriction_mask_ = PathfindingFlag::RestrictHuge; + break; + default: + break; } } - - if( settings.avoid_dangerous_fields && ( p_special & PF_FIELD ) ) { - // We'll walk through even known-dangerous fields if we absolutely have to. - return 500; - } - - return 0; + size_restriction_ = size_restriction; } -int map::extra_cost( const tripoint &cur, const tripoint &p, const pathfinding_settings &settings, - pf_special p_special ) const +int PathfindingSettings::bash_rating_from_range( int min, int max ) const { - int pass_cost = cost_to_pass( cur, p, settings, p_special ); - if( pass_cost < 0 ) { - return pass_cost; - } - - int avoid_cost = cost_to_avoid( cur, p, settings, p_special ); - if( avoid_cost < 0 ) { - return avoid_cost; - } - return pass_cost + avoid_cost; + if( avoid_bashing_ ) { + return 0; + } + // TODO: Move all the bash stuff to map so this logic isn't duplicated. + ///\EFFECT_STR increases smashing damage + if( bash_strength_ < min ) { + return 0; + } else if( bash_strength_ >= max ) { + return 10; + } + const double ret = ( 10.0 * ( bash_strength_ - min ) ) / ( max - min ); + // Round up to 1, so that desperate NPCs can try to bash down walls + return std::max( ret, 1.0 ); } -std::vector map::route( const tripoint &f, const tripoint &t, - const pathfinding_settings &settings, - const std::function &avoid ) const +std::pair +RealityBubblePathfinder::FastTripointSet::emplace( const tripoint_bub_ms &p ) { - /* TODO: If the origin or destination is out of bound, figure out the closest - * in-bounds point and go to that, then to the real origin/destination. - */ - std::vector ret; - - if( f == t || !inbounds( f ) ) { - return ret; - } + const int z = p.z() + OVERMAP_DEPTH; + dirty_[z] = true; + // Note that this is a reference into the bitset, despite not being a reference. + // NOLINTNEXTLINE(cata-almost-never-auto) + auto ref = set_[z][p.y() * MAPSIZE_X + p.x()]; + const bool missing = !ref; + ref = true; + return std::make_pair( NotIterator(), missing ); +} - if( !inbounds( t ) ) { - tripoint clipped = t; - clip_to_bounds( clipped ); - return route( f, clipped, settings, avoid ); - } - // First, check for a simple straight line on flat ground - // Except when the line contains a pre-closed tile - we need to do regular pathing then - if( f.z == t.z ) { - auto line_path = straight_route( f, t ); - if( !line_path.empty() ) { - if( std::none_of( line_path.begin(), line_path.end(), avoid ) ) { - return line_path; - } +void RealityBubblePathfinder::FastTripointSet::clear() +{ + for( int z = 0; z < OVERMAP_LAYERS; ++z ) { + if( !dirty_[z] ) { + continue; } + dirty_[z] = false; + set_[z].reset(); } +} - // If expected path length is greater than max distance, allow only line path, like above - if( rl_dist( f, t ) > settings.max_dist ) { - return ret; - } +std::pair*, bool> +RealityBubblePathfinder::FastBestPathMap::try_emplace( const tripoint_bub_ms &child, int cost, + const tripoint_bub_ms &parent ) +{ + std::pair &result = best_states_[child.z() + + OVERMAP_DEPTH][child.y()][child.x()]; + if( const auto [_, inserted] = in_.emplace( child ); inserted ) { + result.first = cost; + result.second = parent; + return std::make_pair( &result, true ); + } + return std::make_pair( &result, false ); +} - const int max_length = settings.max_length; +namespace +{ - const int pad = 16; // Should be much bigger - low value makes pathfinders dumb! - tripoint min( std::min( f.x, t.x ) - pad, std::min( f.y, t.y ) - pad, std::min( f.z, t.z ) ); - tripoint max( std::max( f.x, t.x ) + pad, std::max( f.y, t.y ) + pad, std::max( f.z, t.z ) ); - clip_to_bounds( min.x, min.y, min.z ); - clip_to_bounds( max.x, max.y, max.z ); +constexpr int one_axis = 50; +constexpr int two_axis = 71; +constexpr int three_axis = 87; - pf.reset( min.z, max.z ); +int adjacent_octile_distance( const tripoint_bub_ms &from, const tripoint_bub_ms &to ) +{ + switch( std::abs( from.x() - to.x() ) + std::abs( from.y() - to.y() ) + std::abs( + from.z() - to.z() ) ) { + case 1: + return one_axis; + case 2: + return two_axis; + case 3: + return three_axis; + default: + return 0; + } +} - pf.add_point( 0, 0, f, f ); +int octile_distance( const tripoint_bub_ms &from, const tripoint_bub_ms &to ) +{ + const tripoint d( std::abs( from.x() - to.x() ), std::abs( from.y() - to.y() ), + std::abs( from.z() - to.z() ) ); + const int min = std::min( d.x, std::min( d.y, d.z ) ); + const int max = std::max( d.x, std::max( d.y, d.z ) ); + const int mid = d.x + d.y + d.z - min - max; + return ( three_axis - two_axis ) * min + ( two_axis - one_axis ) * mid + one_axis * max; +} - bool done = false; +int adjacent_distance_metric( const tripoint_bub_ms &from, const tripoint_bub_ms &to ) +{ + return trigdist ? adjacent_octile_distance( from, to ) : 50 * square_dist( from, to ); +} - do { - tripoint cur = pf.get_next(); +int distance_metric( const tripoint_bub_ms &from, const tripoint_bub_ms &to ) +{ + return trigdist ? octile_distance( from, to ) : 50 * square_dist( from, to ); +} - const int parent_index = flat_index( cur.xy() ); - path_data_layer &layer = pf.get_layer( cur.z ); - if( layer.closed[parent_index] ) { - continue; - } +std::optional position_cost( const map &here, const tripoint_bub_ms &p, + const PathfindingSettings &settings, const RealityBubblePathfindingCache &cache ) +{ + const PathfindingFlags flags = cache.flags( p ); + if( flags & settings.avoid_mask() ) { + return std::nullopt; + } - if( layer.gscore[parent_index] > max_length ) { - // Shortest path would be too long, return empty vector - return std::vector(); + std::optional cost; + if( flags & ( PathfindingFlag::Obstacle | PathfindingSettings::AnySizeRestriction ) ) { + if( flags.is_set( PathfindingFlag::Obstacle ) ) { + if( settings.is_digging() ) { + if( !flags.is_set( PathfindingFlag::Burrowable ) ) { + return std::nullopt; + } + cost = 0; + } else { + if( flags.is_set( PathfindingFlag::Door ) && !settings.avoid_opening_doors() && + ( !flags.is_set( PathfindingFlag::LockedDoor ) || !settings.avoid_unlocking_doors() ) ) { + const int this_cost = flags.is_set( PathfindingFlag::LockedDoor ) ? 4 : 2; + cost = std::min( this_cost, cost.value_or( this_cost ) ); + } + if( flags.is_set( PathfindingFlag::Climbable ) && !settings.avoid_climbing() && + settings.climb_cost() > 0 ) { + // Climbing fences + const int this_cost = settings.climb_cost(); + cost = std::min( this_cost, cost.value_or( this_cost ) ); + } + } } - if( cur == t ) { - done = true; - break; + if( flags.is_set( PathfindingFlag::Bashable ) ) { + const auto [bash_min, bash_max] = cache.bash_range( p ); + const int bash_rating = settings.bash_rating_from_range( bash_min, bash_max ); + if( bash_rating >= 1 ) { + // Expected number of turns to bash it down + const int this_cost = 20 / bash_rating; + cost = std::min( this_cost, cost.value_or( this_cost ) ); + } } - layer.closed[parent_index] = true; - - const pathfinding_cache &pf_cache = get_pathfinding_cache_ref( cur.z ); - const pf_special cur_special = pf_cache.special[cur.x][cur.y]; - - // 7 3 5 - // 1 . 2 - // 6 4 8 - constexpr std::array x_offset{{ -1, 1, 0, 0, 1, -1, -1, 1 }}; - constexpr std::array y_offset{{ 0, 0, -1, 1, -1, 1, -1, 1 }}; - for( size_t i = 0; i < 8; i++ ) { - const tripoint p( cur.x + x_offset[i], cur.y + y_offset[i], cur.z ); - const int index = flat_index( p.xy() ); - - // TODO: Remove this and instead have sentinels at the edges - if( p.x < min.x || p.x >= max.x || p.y < min.y || p.y >= max.y ) { - continue; + // Don't check size restrictions if we can bash it down, open it, or climb over it. + if( !cost && ( flags & PathfindingSettings::AnySizeRestriction ) ) { + // Any tile with a size restriction is passable otherwise. + if( flags & settings.size_restriction_mask() ) { + return std::nullopt; } + cost = 0; + } - if( p != t && avoid( p ) ) { - layer.closed[index] = true; - continue; - } + if( !cost ) { + // Can't enter the tile at all. + return std::nullopt; + } + } else { + cost = 0; + } - if( layer.closed[index] ) { - continue; + const auto &maybe_avoid_dangerous_fields_fn = settings.maybe_avoid_dangerous_fields_fn(); + if( flags.is_set( PathfindingFlag::DangerousField ) && maybe_avoid_dangerous_fields_fn ) { + const field &target_field = here.field_at( p.raw() ); + for( const auto &dfield : target_field ) { + if( dfield.second.is_dangerous() && maybe_avoid_dangerous_fields_fn( dfield.first ) ) { + return std::nullopt; } + } + } - // Penalize for diagonals or the path will look "unnatural" - int newg = layer.gscore[parent_index] + ( ( cur.x != p.x && cur.y != p.y ) ? 1 : 0 ); - - const pf_special p_special = pf_cache.special[p.x][p.y]; - const int cost = extra_cost( cur, p, settings, p_special ); - if( cost < 0 ) { - if( cost == PF_IMPASSABLE ) { - layer.closed[index] = true; - } - continue; - } - newg += cost; - - // Special case: pathfinders that avoid traps can avoid ledges by - // climbing down. This can't be covered by |extra_cost| because it - // can add a new point to the search. - if( settings.avoid_traps && ( p_special & PF_TRAP ) ) { - const const_maptile &tile = maptile_at_internal( p ); - const ter_t &terrain = tile.get_ter_t(); - const trap &ter_trp = terrain.trap.obj(); - const trap &trp = ter_trp.is_benign() ? tile.get_trap_t() : ter_trp; - if( !trp.is_benign() && terrain.has_flag( ter_furn_flag::TFLAG_NO_FLOOR ) ) { - // Warning: really expensive, needs a cache - tripoint below( p.xy(), p.z - 1 ); - if( valid_move( p, below, false, true ) ) { - if( !has_flag( ter_furn_flag::TFLAG_NO_FLOOR, below ) ) { - // Otherwise this would have been a huge fall - path_data_layer &layer = pf.get_layer( p.z - 1 ); - // From cur, not p, because we won't be walking on air - pf.add_point( layer.gscore[parent_index] + 10, - layer.score[parent_index] + 10 + 2 * rl_dist( below, t ), - cur, below ); - } - - // Close p, because we won't be walking on it - layer.closed[index] = true; - continue; - } - } - } + const auto &maybe_avoid_fn = settings.maybe_avoid_fn(); + if( maybe_avoid_fn && maybe_avoid_fn( p ) ) { + return std::nullopt; + } - pf.add_point( newg, newg + 2 * rl_dist( p, t ), cur, p ); - } + return *cost * 50; +} - if( !( cur_special & PF_UPDOWN ) || !settings.allow_climb_stairs ) { - // The part below is only for z-level pathing - continue; +std::optional transition_cost( const map &here, const tripoint_bub_ms &from, + const tripoint_bub_ms &to, const PathfindingSettings &settings, + const RealityBubblePathfindingCache &cache ) +{ + const PathfindingFlags from_flags = cache.flags( from ); + const bool is_falling = from_flags.is_set( PathfindingFlag::Air ) && !settings.is_flying(); + if( is_falling ) { + // Can only fall straight down. + if( from.z() < to.z() || from.xy() != to.xy() ) { + return std::nullopt; } + } - bool rope_ladder = false; - const const_maptile &parent_tile = maptile_at_internal( cur ); - const ter_t &parent_terrain = parent_tile.get_ter_t(); - if( settings.allow_climb_stairs && cur.z > min.z && - parent_terrain.has_flag( ter_furn_flag::TFLAG_GOES_DOWN ) ) { - std::optional opt_dest = g->find_or_make_stairs( get_map(), - cur.z - 1, rope_ladder, false, cur ); - if( !opt_dest ) { - continue; + const bool is_vertical_movement = from.z() != to.z(); + if( !is_falling && is_vertical_movement ) { + const tripoint_bub_ms &upper = from.z() > to.z() ? from : to; + const tripoint_bub_ms &lower = from.z() < to.z() ? from : to; + if( cache.flags( lower ).is_set( PathfindingFlag::GoesUp ) && + cache.flags( upper ).is_set( PathfindingFlag::GoesDown ) ) { + if( settings.avoid_climb_stairway() ) { + return std::nullopt; } - tripoint dest = opt_dest.value(); - if( vertical_move_destination( *this, ter_furn_flag::TFLAG_GOES_UP, dest ) ) { - if( !inbounds( dest ) ) { - continue; - } - path_data_layer &layer = pf.get_layer( dest.z ); - pf.add_point( layer.gscore[parent_index] + 2, - layer.score[parent_index] + 2 * rl_dist( dest, t ), - cur, dest ); + // Stairs can teleport us, so we need to use non-adjacent calc. + return 2 * distance_metric( from, to ); + } else if( settings.is_flying() ) { + const tripoint_bub_ms above_lower( lower.xy(), lower.z() + 1 ); + if( !( cache.flags( upper ).is_set( PathfindingFlag::Air ) || + cache.flags( above_lower ).is_set( PathfindingFlag::Air ) ) ) { + return std::nullopt; } - } - if( settings.allow_climb_stairs && cur.z < max.z && - parent_terrain.has_flag( ter_furn_flag::TFLAG_GOES_UP ) ) { - std::optional opt_dest = g->find_or_make_stairs( get_map(), - cur.z + 1, rope_ladder, false, cur ); - if( !opt_dest ) { - continue; + } else if( to.z() > from.z() ) { + if( !cache.flags( to ).is_set( PathfindingFlag::RampDown ) ) { + return std::nullopt; } - tripoint dest = opt_dest.value(); - if( vertical_move_destination( *this, ter_furn_flag::TFLAG_GOES_DOWN, dest ) ) { - if( !inbounds( dest ) ) { - continue; - } - path_data_layer &layer = pf.get_layer( dest.z ); - pf.add_point( layer.gscore[parent_index] + 2, - layer.score[parent_index] + 2 * rl_dist( dest, t ), - cur, dest ); + } else if( to.z() < from.z() ) { + if( !cache.flags( to ).is_set( PathfindingFlag::RampUp ) ) { + return std::nullopt; } } - if( cur.z < max.z && parent_terrain.has_flag( ter_furn_flag::TFLAG_RAMP ) && - valid_move( cur, tripoint( cur.xy(), cur.z + 1 ), false, true ) ) { - path_data_layer &layer = pf.get_layer( cur.z + 1 ); - for( size_t it = 0; it < 8; it++ ) { - const tripoint above( cur.x + x_offset[it], cur.y + y_offset[it], cur.z + 1 ); - if( !inbounds( above ) ) { - continue; - } - pf.add_point( layer.gscore[parent_index] + 4, - layer.score[parent_index] + 4 + 2 * rl_dist( above, t ), - cur, above ); - } + } + + const PathfindingFlags to_flags = cache.flags( to ); + if( to_flags.is_set( PathfindingFlag::Obstacle ) ) { + // Can't interact with obstacles across z-levels. + // TODO: allow this + if( is_vertical_movement ) { + return std::nullopt; } - if( cur.z < max.z && parent_terrain.has_flag( ter_furn_flag::TFLAG_RAMP_UP ) && - valid_move( cur, tripoint( cur.xy(), cur.z + 1 ), false, true, true ) ) { - path_data_layer &layer = pf.get_layer( cur.z + 1 ); - for( size_t it = 0; it < 8; it++ ) { - const tripoint above( cur.x + x_offset[it], cur.y + y_offset[it], cur.z + 1 ); - if( !inbounds( above ) ) { - continue; + if( !settings.is_digging() ) { + if( to_flags.is_set( PathfindingFlag::Door ) && !settings.avoid_opening_doors() && + ( !to_flags.is_set( PathfindingFlag::LockedDoor ) || !settings.avoid_unlocking_doors() ) ) { + const bool is_inside_door = to_flags.is_set( PathfindingFlag::InsideDoor ); + if( is_inside_door ) { + int dummy; + const bool is_vehicle = to_flags.is_set( PathfindingFlag::Vehicle ); + const bool is_outside = is_vehicle ? here.veh_at_internal( from.raw(), + dummy ) != here.veh_at_internal( to.raw(), dummy ) : here.is_outside( from.raw() ); + if( is_outside ) { + return std::nullopt; + } } - pf.add_point( layer.gscore[parent_index] + 4, - layer.score[parent_index] + 4 + 2 * rl_dist( above, t ), - cur, above ); } } - if( cur.z > min.z && parent_terrain.has_flag( ter_furn_flag::TFLAG_RAMP_DOWN ) && - valid_move( cur, tripoint( cur.xy(), cur.z - 1 ), false, true, true ) ) { - path_data_layer &layer = pf.get_layer( cur.z - 1 ); - for( size_t it = 0; it < 8; it++ ) { - const tripoint below( cur.x + x_offset[it], cur.y + y_offset[it], cur.z - 1 ); - if( !inbounds( below ) ) { - continue; - } - pf.add_point( layer.gscore[parent_index] + 4, - layer.score[parent_index] + 4 + 2 * rl_dist( below, t ), - cur, below ); - } + } else if( to_flags.is_set( PathfindingFlag::Air ) ) { + // This is checking horizontal movement only. + if( settings.avoid_falling() && !settings.is_flying() ) { + return std::nullopt; } + } - } while( !done && !pf.empty() ); + // TODO: Move the move cost cache into map so this logic isn't duplicated. + const int mult = adjacent_distance_metric( from, to ); - if( done ) { - ret.reserve( rl_dist( f, t ) * 2 ); - tripoint cur = t; - // Just to limit max distance, in case something weird happens - for( int fdist = max_length; fdist != 0; fdist-- ) { - const int cur_index = flat_index( cur.xy() ); - const path_data_layer &layer = pf.get_layer( cur.z ); - const tripoint &par = layer.parent[cur_index]; - if( cur == f ) { - break; - } + // Flying monsters aren't slowed down by terrain. + if( settings.is_flying() ) { + return 2 * mult; + } - ret.push_back( cur ); - // Jumps are acceptable on 1 z-level changes - // This is because stairs teleport the player too - if( rl_dist( cur, par ) > 1 && std::abs( cur.z - par.z ) != 1 ) { - debugmsg( "Jump in our route! %d:%d:%d->%d:%d:%d", - cur.x, cur.y, cur.z, par.x, par.y, par.z ); - return ret; - } + const int cost = cache.move_cost( from ) + cache.move_cost( to ); + return static_cast( mult * cost ) / 2; +} + +} // namespace + + +bool map::can_teleport( const tripoint_bub_ms &to, const PathfindingSettings &settings ) const +{ + if( !inbounds( to ) ) { + return false; + } + pathfinding_cache()->update( *this, to.z(), to.z() ); + return position_cost( *this, to, settings, *pathfinding_cache() ).has_value(); +} + +bool map::can_move( const tripoint_bub_ms &from, const tripoint_bub_ms &to, + const PathfindingSettings &settings ) const +{ + if( !inbounds( from ) || !inbounds( to ) ) { + return false; + } + if( from == to ) { + return true; + } + pathfinding_cache()->update( *this, std::min( from.z(), to.z() ), std::max( from.z(), to.z() ) ); + if( position_cost( *this, to, settings, *pathfinding_cache() ).has_value() ) { + return transition_cost( *this, from, to, settings, *pathfinding_cache() ).has_value(); + } + return false; +} + +std::optional map::move_cost( const tripoint_bub_ms &from, const tripoint_bub_ms &to, + const PathfindingSettings &settings ) const +{ + if( !inbounds( from ) || !inbounds( to ) ) { + return std::nullopt; + } + if( from == to ) { + return 0; + } + pathfinding_cache()->update( *this, std::min( from.z(), to.z() ), std::max( from.z(), to.z() ) ); + if( const std::optional p_cost = position_cost( *this, to, settings, *pathfinding_cache() ) ) { + if( const std::optional t_cost = transition_cost( *this, from, to, settings, + *pathfinding_cache() ) ) { + return *p_cost + *t_cost; + } + } + return std::nullopt; +} - cur = par; +std::vector map::straight_route( const tripoint_bub_ms &from, + const tripoint_bub_ms &to, + const PathfindingSettings &settings ) const +{ + + if( from == to || !inbounds( from ) || !inbounds( to ) ) { + return {}; + } + + RealityBubblePathfindingCache &cache = *pathfinding_cache(); + const int pad = settings.rb_settings().padding().z(); + cache.update( *this, std::min( from.z(), to.z() ) - pad, std::max( from.z(), to.z() ) + pad ); + + std::vector line_path = line_to( from, to ); + const PathfindingFlags avoid = settings.avoid_mask() | PathfindingSettings::RoughTerrain; + // Check all points for all fast avoidance. + if( !std::any_of( line_path.begin(), line_path.end(), [&cache, avoid]( const tripoint_bub_ms & p ) { + return cache.flags( p ) & avoid; + } ) ) { + // Now do the slow check. Check if all the positions are valid. + if( std::all_of( line_path.begin(), line_path.end(), [this, &cache, + &settings]( const tripoint_bub_ms & p ) { + return position_cost( *this, p, settings, cache ); + } ) ) { + // Now check that all the transitions between each position are valid. + const tripoint_bub_ms *prev = &from; + if( std::find_if_not( line_path.begin(), line_path.end(), [this, &prev, &cache, + &settings]( const tripoint_bub_ms & p ) { + return transition_cost( *this, *std::exchange( prev, &p ), p, settings, cache ).has_value(); + } ) == line_path.end() ) { + return line_path; + } } + } + return {}; +} - std::reverse( ret.begin(), ret.end() ); +std::vector map::route( const tripoint_bub_ms &from, const tripoint_bub_ms &to, + const PathfindingSettings &settings ) const +{ + if( from == to || !inbounds( from ) || !inbounds( to ) ) { + return {}; } + RealityBubblePathfindingCache &cache = *pathfinding_cache(); + const int pad = settings.rb_settings().padding().z(); + cache.update( *this, std::min( from.z(), to.z() ) - pad, std::max( from.z(), to.z() ) + pad ); + + // First, check for a simple straight line on flat ground. + if( from.z() == to.z() ) { + std::vector line_path = straight_route( from, to, settings ); + if( !line_path.empty() ) { + return line_path; + } + } + + // If expected path length is greater than max distance, allow only line path, like above + if( rl_dist( from, to ) > settings.max_distance() ) { + return {}; + } + + return pathfinder()->find_path( settings.rb_settings(), from, to, + [this, &settings, &cache]( const tripoint_bub_ms & p ) { + return position_cost( *this, p, settings, cache ); + }, + [this, &settings, &cache]( const tripoint_bub_ms & from, const tripoint_bub_ms & to ) { + return transition_cost( *this, from, to, settings, cache ); + }, + []( const tripoint_bub_ms & from, const tripoint_bub_ms & to ) { + return 2 * distance_metric( from, to ); + } ); +} + +std::vector map::route( const tripoint &f, const tripoint &t, + const pathfinding_settings &settings ) +{ + const PathfindingSettings pf_settings = settings.to_new_pathfinding_settings(); + // TODO: Get rid of this. + const tripoint_bub_ms from = tripoint_bub_ms::make_unchecked( f ); + const tripoint_bub_ms to = tripoint_bub_ms::make_unchecked( t ); + std::vector bub_route = route( from, to, pf_settings ); + std::vector ret( bub_route.size() ); + for( tripoint_bub_ms p : bub_route ) { + ret.push_back( p.raw() ); + } return ret; } std::vector map::route( const tripoint_bub_ms &f, const tripoint_bub_ms &t, - const pathfinding_settings &settings, - const std::function &avoid ) const + const pathfinding_settings &settings ) const { - std::vector raw_result = route( f.raw(), t.raw(), settings, avoid ); - std::vector result; - std::transform( raw_result.begin(), raw_result.end(), std::back_inserter( result ), - []( const tripoint & p ) { - return tripoint_bub_ms( p ); - } ); - return result; + const PathfindingSettings pf_settings = settings.to_new_pathfinding_settings(); + return route( f, t, pf_settings ); +} + +PathfindingSettings pathfinding_settings::to_new_pathfinding_settings() const +{ + PathfindingSettings settings; + settings.set_bash_strength( bash_strength ); + settings.set_max_distance( max_dist ); + settings.set_max_cost( max_length * 50 ); + settings.set_climb_cost( climb_cost ); + settings.set_avoid_opening_doors( !allow_open_doors ); + settings.set_avoid_unlocking_doors( !allow_unlock_doors ); + settings.set_avoid_dangerous_traps( avoid_traps ); + if( avoid_rough_terrain ) { + settings.set_avoid_rough_terrain( true ); + } + settings.set_avoid_sharp( avoid_sharp ); + return settings; } diff --git a/src/pathfinding.h b/src/pathfinding.h index e62769ae1042d..759ff6d263ebe 100644 --- a/src/pathfinding.h +++ b/src/pathfinding.h @@ -2,53 +2,728 @@ #ifndef CATA_SRC_PATHFINDING_H #define CATA_SRC_PATHFINDING_H -#include "coords_fwd.h" +#include "a_star.h" +#include "coordinates.h" +#include "creature.h" #include "game_constants.h" -#include "mdarray.h" - -enum pf_special : int { - PF_NORMAL = 0x00, // Plain boring tile (grass, dirt, floor etc.) - PF_SLOW = 0x01, // Tile with move cost >2 - PF_WALL = 0x02, // Unpassable ter/furn/vehicle - PF_VEHICLE = 0x04, // Any vehicle tile (passable or not) - PF_FIELD = 0x08, // Dangerous field - PF_TRAP = 0x10, // Dangerous trap - PF_UPDOWN = 0x20, // Stairs, ramp etc. - PF_CLIMBABLE = 0x40, // 0 move cost but can be climbed on examine - PF_SHARP = 0x80, // sharp items (barbed wire, etc) +#include "mapdata.h" +#include "type_id.h" + +class map; + +enum class PathfindingFlag : uint8_t { + Ground = 0, // Can walk on + Slow, // Move cost > 2 + Swimmable, // Can swim in + Air, // Empty air + Unsheltered, // Outside and above ground level + Obstacle, // Something stopping us, might be bashable. + Bashable, // Something bashable. + Impassable, // Impassable obstacle. + Vehicle, // Vehicle tile (passable or not) + DangerousField, // Dangerous field + DangerousTrap, // Dangerous trap (i.e. not flagged benign) + GoesUp, // Valid stairs up + GoesDown, // Valid stairs down + RampUp, // Valid ramp up + RampDown, // Valid ramp down + Climbable, // Obstacle but can be climbed on examine + Sharp, // Sharp items (barbed wire, etc) + Door, // A door (any kind) + InsideDoor, // A door that can be opened from the inside only + LockedDoor, // A locked door + Pit, // A pit you can fall into / climb out of. + DeepWater, // Deep water. + Burrowable, // Can burrow into + HardGround, // Can not dig & burrow intotiny = 1, + RestrictTiny, // Tiny cannot enter + RestrictSmall, // Small cannot enter + RestrictMedium, // Medium cannot enter + RestrictLarge, // Large cannot enter + RestrictHuge, // Huge cannot enter + Lava, // Lava terrain +}; + +class PathfindingFlags +{ + public: + constexpr PathfindingFlags() = default; + + // NOLINTNEXTLINE(google-explicit-constructor) + constexpr PathfindingFlags( PathfindingFlag flag ) : flags_( uint32_t{ 1 } << static_cast + ( flag ) ) {} + + constexpr void set_union( PathfindingFlags flags ) { + flags_ |= flags.flags_; + } + constexpr void set_intersect( PathfindingFlags flags ) { + flags_ &= flags.flags_; + } + constexpr void set_clear( PathfindingFlags flags ) { + flags_ &= ~flags.flags_; + } + + constexpr void clear() { + flags_ = 0; + } + + constexpr bool is_set( PathfindingFlag flag ) const { + return flags_ & ( uint32_t{ 1 } << static_cast( flag ) ); + } + constexpr bool is_set( PathfindingFlags flags ) const { + return ( flags_ & flags.flags_ ) == flags.flags_; + } + constexpr bool is_any_set() const { + return flags_; + } + + constexpr explicit operator bool() const { + return is_any_set(); + } + + constexpr PathfindingFlags &operator|=( PathfindingFlags flags ) { + set_union( flags ); + return *this; + } + + constexpr PathfindingFlags &operator&=( PathfindingFlags flags ) { + set_intersect( flags ); + return *this; + } + + private: + uint32_t flags_ = 0; }; -constexpr pf_special operator | ( pf_special lhs, pf_special rhs ) +constexpr PathfindingFlags operator|( PathfindingFlags lhs, PathfindingFlags rhs ) { - return static_cast( static_cast< int >( lhs ) | static_cast< int >( rhs ) ); + return lhs |= rhs; } -constexpr pf_special operator & ( pf_special lhs, pf_special rhs ) +constexpr PathfindingFlags operator&( PathfindingFlags lhs, PathfindingFlags rhs ) { - return static_cast( static_cast< int >( lhs ) & static_cast< int >( rhs ) ); + return lhs &= rhs; } -inline pf_special &operator |= ( pf_special &lhs, pf_special rhs ) +constexpr PathfindingFlags operator|( PathfindingFlags lhs, PathfindingFlag rhs ) { - lhs = static_cast( static_cast< int >( lhs ) | static_cast< int >( rhs ) ); - return lhs; + return lhs |= rhs; } -inline pf_special &operator &= ( pf_special &lhs, pf_special rhs ) +constexpr PathfindingFlags operator&( PathfindingFlags lhs, PathfindingFlag rhs ) { - lhs = static_cast( static_cast< int >( lhs ) & static_cast< int >( rhs ) ); - return lhs; + return lhs &= rhs; } -struct pathfinding_cache { - pathfinding_cache(); +// Note that this is in reverse order for memory locality: z, y, x. +template +using RealityBubbleArray = + std::array, MAPSIZE_Y>, OVERMAP_LAYERS>; + +class RealityBubblePathfindingCache +{ + public: + RealityBubblePathfindingCache(); + + PathfindingFlags flags( const tripoint_bub_ms &p ) const { + return flag_cache_[p.z() + OVERMAP_DEPTH][p.y()][p.x()]; + } + + int move_cost( const tripoint_bub_ms &p ) const { + return move_cost_cache_[p.z() + OVERMAP_DEPTH][p.y()][p.x()]; + } + + const std::pair &bash_range( const tripoint_bub_ms &p ) const { + return bash_range_cache_[p.z() + OVERMAP_DEPTH][p.y()][p.x()]; + } + + const tripoint_bub_ms &up_destination( const tripoint_bub_ms &p ) const { + return up_destinations_.find( p )->second; + } + + const tripoint_bub_ms &down_destination( const tripoint_bub_ms &p ) const { + return down_destinations_.find( p )->second; + } + + void update( const map &here, int min_z, int max_z ); + + void invalidate( int z ) { + dirty_z_levels_.emplace( z ); + } + + void invalidate( const tripoint_bub_ms &p ) { + dirty_positions_[p.z()].emplace( p.xy() ); + invalidate_dependants( p ); + } + + private: + PathfindingFlags &flags_ref( const tripoint_bub_ms &p ) { + return flag_cache_[p.z() + OVERMAP_DEPTH][p.y()][p.x()]; + } + + char &move_cost_ref( const tripoint_bub_ms &p ) { + return move_cost_cache_[p.z() + OVERMAP_DEPTH][p.y()][p.x()]; + } + + std::pair &bash_range_ref( const tripoint_bub_ms &p ) { + return bash_range_cache_[p.z() + OVERMAP_DEPTH][p.y()][p.x()]; + } + + bool vertical_move_destination( const map &here, ter_furn_flag flag, tripoint &t ) const; + + void invalidate_dependants( const tripoint_bub_ms &p ); + + void update( const map &here, const tripoint_bub_ms &p ); + + std::unordered_set dirty_z_levels_; + std::unordered_map> dirty_positions_; + std::unordered_map> dependants_by_position_; + + std::unordered_map up_destinations_; + std::unordered_map down_destinations_; + RealityBubbleArray flag_cache_; + RealityBubbleArray move_cost_cache_; + RealityBubbleArray> bash_range_cache_; +}; + +class RealityBubblePathfindingSettings +{ + public: + bool allow_flying() const { + return allow_flying_; + } + void set_allow_flying( bool v = true ) { + allow_flying_ = v; + } + + bool allow_falling() const { + return allow_falling_; + } + void set_allow_falling( bool v = true ) { + allow_falling_ = v; + } + + bool allow_stairways() const { + return allow_stairways_; + } + void set_allow_stairways( bool v = true ) { + allow_stairways_ = v; + } + + int max_cost() const { + return max_cost_; + } + void set_max_cost( int v = 0 ) { + max_cost_ = v; + } + + const tripoint_rel_ms &padding() const { + return padding_; + } + void set_padding( const tripoint_rel_ms &v ) { + padding_ = v; + } + + private: + bool allow_flying_ = false; + bool allow_falling_ = false; + bool allow_stairways_ = false; + int max_cost_ = 0; + tripoint_rel_ms padding_ = tripoint_rel_ms( 16, 16, 1 ); +}; + +class RealityBubblePathfinder +{ + public: + // The class uses a lot of memory, and is safe to reuse as long as none of the provided + // functors to find_path make use of it. + explicit RealityBubblePathfinder( RealityBubblePathfindingCache *cache ) : cache_( cache ) {} + + template + std::vector find_path( const RealityBubblePathfindingSettings &settings, + const tripoint_bub_ms &from, const tripoint_bub_ms &to, + PositionCostFn &&p_cost_fn, MoveCostFn &&m_cost_fn, HeuristicFn &&heuristic_fn ); + + private: + // Minimum implementation of std::unordered_set interface that is used in the pathfinder. + class FastTripointSet + { + public: + // Empty dummy type to return in place of an iterator. It isn't possible to give a nice + // iterator implementation, and the pathfinder doesn't use it anyways. + struct NotIterator {}; + + std::pair emplace( const tripoint_bub_ms &p ); + + void clear(); + + std::size_t count( const tripoint_bub_ms &p ) const { + return set_[p.z() + OVERMAP_DEPTH][p.y() * MAPSIZE_X + p.x()]; + } + + private: + std::array dirty_ = {}; + std::array, OVERMAP_LAYERS> set_ = {}; + }; + + // Minimum implementation of std::unordered_map interface that is used in the pathfinder. + class FastBestPathMap + { + public: + std::pair*, bool> try_emplace( const tripoint_bub_ms &child, + int cost, const tripoint_bub_ms &parent ); + + std::size_t count( const tripoint_bub_ms &p ) const { + return in_.count( p ); + } + + void clear() { + in_.clear(); + } + + const std::pair &at( const tripoint_bub_ms &child ) const { + return best_states_[child.z() + OVERMAP_DEPTH][child.y()][child.x()]; + } + + std::pair &operator[]( const tripoint_bub_ms &child ) { + return *try_emplace( child, 0, child ).first; + } + + private: + FastTripointSet in_; + RealityBubbleArray> best_states_; + }; + + template + std::vector find_path_impl( AStar &impl, + const RealityBubblePathfindingSettings &settings, + const tripoint_bub_ms &from, const tripoint_bub_ms &to, + PositionCostFn &&p_cost_fn, MoveCostFn &&m_cost_fn, HeuristicFn &&heuristic_fn ); - bool dirty = false; - std::unordered_set dirty_points; + RealityBubblePathfindingCache *cache_; + AStarPathfinder astar_; + BidirectionalAStarPathfinder + bidirectional_astar_; +}; + +class PathfindingSettings +{ + public: + static constexpr PathfindingFlags RoughTerrain = PathfindingFlag::Slow | PathfindingFlag::Obstacle | + PathfindingFlag::Vehicle | PathfindingFlag::Sharp | PathfindingFlag::DangerousTrap; + static constexpr PathfindingFlags AnySizeRestriction = PathfindingFlag::RestrictTiny | + PathfindingFlag::RestrictSmall | + PathfindingFlag::RestrictMedium | PathfindingFlag::RestrictLarge | PathfindingFlag::RestrictHuge; + + bool avoid_falling() const { + return !rb_settings_.allow_falling(); + } + void set_avoid_falling( bool v = true ) { + rb_settings_.set_allow_falling( !v ); + } + + bool avoid_unsheltered() const { + return is_set( PathfindingFlag::Unsheltered ); + } + void set_avoid_unsheltered( bool v = true ) { + set( PathfindingFlag::Unsheltered, v ); + } + + bool avoid_swimming() const { + return is_set( PathfindingFlag::Swimmable ); + } + void set_avoid_swimming( bool v = true ) { + set( PathfindingFlag::Swimmable, v ); + } + + bool avoid_ground() const { + return is_set( PathfindingFlag::Ground ); + } + void set_avoid_ground( bool v = true ) { + set( PathfindingFlag::Ground, v ); + } + + bool avoid_vehicle() const { + return is_set( PathfindingFlag::Vehicle ); + } + void set_avoid_vehicle( bool v = true ) { + set( PathfindingFlag::Vehicle, v ); + } + + bool avoid_climbing() const { + return avoid_climbing_; + } + void set_avoid_climbing( bool v = true ) { + avoid_climbing_ = v; + maybe_set_avoid_obstacle(); + } + + bool avoid_climb_stairway() const { + return !rb_settings_.allow_stairways(); + } + void set_avoid_climb_stairway( bool v = true ) { + rb_settings_.set_allow_stairways( !v ); + } + + bool avoid_deep_water() const { + return is_set( PathfindingFlag::DeepWater ); + } + void set_avoid_deep_water( bool v = true ) { + set( PathfindingFlag::DeepWater, v ); + } + + std::optional size_restriction() const { + return size_restriction_; + } + PathfindingFlags size_restriction_mask() const { + return size_restriction_mask_; + } + void set_size_restriction( std::optional size_restriction = std::nullopt ); + + bool avoid_pits() const { + return is_set( PathfindingFlag::Pit ); + } + void set_avoid_pits( bool v = true ) { + set( PathfindingFlag::Pit, v ); + } + + bool avoid_opening_doors() const { + return avoid_opening_doors_; + } + void set_avoid_opening_doors( bool v = true ) { + avoid_opening_doors_ = v; + maybe_set_avoid_obstacle(); + } + + bool avoid_unlocking_doors() const { + return avoid_unlocking_doors_; + } + void set_avoid_unlocking_doors( bool v = true ) { + avoid_unlocking_doors_ = v; + } + + bool avoid_rough_terrain() const { + return is_set( RoughTerrain ); + } + void set_avoid_rough_terrain( bool v = true ) { + set( RoughTerrain, v ); + } + + bool avoid_dangerous_traps() const { + return is_set( PathfindingFlag::DangerousTrap ); + } + void set_avoid_dangerous_traps( bool v ) { + set( PathfindingFlag::DangerousTrap, v ); + } + + bool avoid_sharp() const { + return is_set( PathfindingFlag::Sharp ); + } + void set_avoid_sharp( bool v = true ) { + set( PathfindingFlag::Sharp, v ); + } + + bool avoid_lava() const { + return is_set( PathfindingFlag::Lava ); + } + void set_avoid_lava( bool v = true ) { + set( PathfindingFlag::Lava, v ); + } + + bool avoid_hard_ground() const { + return is_set( PathfindingFlag::HardGround ); + } + void set_avoid_hard_ground( bool v = true ) { + set( PathfindingFlag::HardGround, v ); + } + + const std::function &maybe_avoid_dangerous_fields_fn() const { + return maybe_avoid_dangerous_fields_fn_; + } + void set_maybe_avoid_dangerous_fields_fn( std::function fn = + nullptr ) { + maybe_avoid_dangerous_fields_fn_ = std::move( fn ); + } + + const std::function &maybe_avoid_fn() const { + return maybe_avoid_fn_; + } + void set_maybe_avoid_fn( std::function fn = nullptr ) { + maybe_avoid_fn_ = std::move( fn ); + } + + int max_distance() const { + return max_distance_; + } + void set_max_distance( int v = 0 ) { + max_distance_ = v; + } + + int max_cost() const { + return rb_settings_.max_cost(); + } + void set_max_cost( int v = 0 ) { + rb_settings_.set_max_cost( v ); + } + + int climb_cost() const { + return climb_cost_; + } + void set_climb_cost( int v = 0 ) { + climb_cost_ = v; + maybe_set_avoid_obstacle(); + } + + bool is_digging() const { + return is_digging_; + } + void set_is_digging( bool v = true ) { + is_digging_ = v; + maybe_set_avoid_obstacle(); + } + + bool is_flying() const { + return rb_settings_.allow_flying(); + } + void set_is_flying( bool v = true ) { + rb_settings_.set_allow_flying( v ); + } + + PathfindingFlags avoid_mask() const { + return avoid_mask_; + } - cata::mdarray special; + bool avoid_bashing() const { + return avoid_bashing_; + } + void set_avoid_bashing( bool v = true ) { + avoid_bashing_ = v; + maybe_set_avoid_obstacle(); + } + + int bash_strength() const { + return bash_strength_; + } + void set_bash_strength( int v = 0 ) { + bash_strength_ = v; + maybe_set_avoid_obstacle(); + } + int bash_rating_from_range( int min, int max ) const; + + protected: + const RealityBubblePathfindingSettings &rb_settings() const { + return rb_settings_; + } + + friend class map; + + private: + bool is_set( PathfindingFlags flag ) const { + return avoid_mask_.is_set( flag ); + } + void set( PathfindingFlags flags, bool v ) { + if( v ) { + avoid_mask_.set_union( flags ); + } else { + avoid_mask_.set_clear( flags ); + } + } + + void maybe_set_avoid_obstacle() { + // Check if we can short circuit checking obstacles. Significantly faster if so. + set( PathfindingFlag::Obstacle, !is_digging() && ( climb_cost() <= 0 || avoid_climbing() ) && + avoid_opening_doors() && ( avoid_bashing() || bash_strength() <= 0 ) ); + } + + PathfindingFlags avoid_mask_ = PathfindingFlag::Impassable; + + std::optional size_restriction_; + PathfindingFlags size_restriction_mask_; + int max_distance_ = 0; + + bool avoid_bashing_ = false; + int bash_strength_ = 0; + + // Expected terrain cost (2 is flat ground) of climbing a wire fence, 0 means no climbing + bool avoid_climbing_ = false; + int climb_cost_ = 0; + + bool is_digging_ = false; + RealityBubblePathfindingSettings rb_settings_; + + bool avoid_opening_doors_ = false; + bool avoid_unlocking_doors_ = false; + std::function maybe_avoid_dangerous_fields_fn_; + std::function maybe_avoid_fn_; }; +// Implementation Details + +template +std::vector RealityBubblePathfinder::find_path( const + RealityBubblePathfindingSettings &settings, const tripoint_bub_ms &from, + const tripoint_bub_ms &to, PositionCostFn &&p_cost_fn, MoveCostFn &&m_cost_fn, + HeuristicFn &&heuristic_fn ) +{ + // The bidirectional pathfinder doesn't handle paths generated by falling, so we have to fall + // back to normal A* in this case. + const bool might_fall = !settings.allow_flying() && settings.allow_falling(); + if( might_fall ) { + return find_path_impl( astar_, settings, from, to, std::forward( p_cost_fn ), + std::forward( m_cost_fn ), std::forward( heuristic_fn ) ); + } else { + return find_path_impl( bidirectional_astar_, settings, from, to, + std::forward( p_cost_fn ), std::forward( m_cost_fn ), + std::forward( heuristic_fn ) ); + } +} + +template +std::vector RealityBubblePathfinder::find_path_impl( AStar &impl, const + RealityBubblePathfindingSettings &settings, const tripoint_bub_ms &from, + const tripoint_bub_ms &to, PositionCostFn &&p_cost_fn, MoveCostFn &&m_cost_fn, + HeuristicFn &&heuristic_fn ) +{ + const tripoint_bub_ms min = coord_max( coord_min( from, to ) - settings.padding(), + tripoint_bub_ms( 0, 0, -OVERMAP_DEPTH ) ); + const tripoint_bub_ms max = coord_min( coord_max( from, to ) + settings.padding(), + tripoint_bub_ms( MAPSIZE_X - 1, MAPSIZE_Y - 1, OVERMAP_HEIGHT ) ); + const inclusive_cuboid bounds( min, max ); + + return impl.find_path( settings.max_cost(), from, to, std::forward( p_cost_fn ), + std::forward( m_cost_fn ), std::forward( heuristic_fn ), [this, &settings, + bounds]( tripoint_bub_ms p, tripoint_bub_ms c, auto &&emit_fn ) { + const tripoint_rel_ms offset( 1, 1, 1 ); + const tripoint_bub_ms min = clamp( c - offset, bounds ); + const tripoint_bub_ms max = clamp( c + offset, bounds ); + if( settings.allow_flying() ) { + for( int z = min.z(); z <= max.z(); ++z ) { + for( int y = min.y(); y <= max.y(); ++y ) { + for( int x = min.x(); x <= max.x(); ++x ) { + if( x == c.x() && y == c.y() && z == c.z() ) { + continue; + } + const tripoint_bub_ms next( x, y, z ); + emit_fn( next ); + } + } + } + return; + } + + const PathfindingFlags flags = cache_->flags( c ); + + // If we're falling, we can only continue falling. + if( c.z() > -OVERMAP_DEPTH && flags.is_set( PathfindingFlag::Air ) ) { + const tripoint_bub_ms down( c.xy(), c.z() - 1 ); + emit_fn( down ); + return; + } + + const point_rel_ms d( ( c.x() > p.x() ) - ( c.x() < p.x() ), + ( c.y() > p.y() ) - ( c.y() < p.y() ) ); + const point_bub_ms n = c.xy() + d; + + if( d.x() != 0 && d.y() != 0 ) { + // Diagonal movement. Visit the following states: + // + // * * n + // . c * + // p . * + // + // where * is a state to be visited + // n is the state in movement direction, visited + // p is the parent state (not visited) + // c is the current state (not visited) + // . is not visited + if( min.x() <= n.x() && n.x() <= max.x() ) { + for( int y = min.y(); y <= max.y(); ++y ) { + const tripoint_bub_ms next( n.x(), y, c.z() ); + emit_fn( next ); + } + } + if( min.y() <= n.y() && n.y() <= max.y() ) { + for( int x = min.x(); x <= max.x(); ++x ) { + if( x == n.x() ) { + continue; + } + const tripoint_bub_ms next( x, n.y(), c.z() ); + emit_fn( next ); + } + } + } else if( d.x() != 0 ) { + // Horizontal movement. Visit the following states: + // + // . . * + // p c n + // . . * + // + // where * is a state to be visited + // n is the state in movement direction, visited + // p is the parent state (not visited) + // c is the current state (not visited) + // . is not visited + if( min.x() <= n.x() && n.x() <= max.x() ) { + for( int y = min.y(); y <= max.y(); ++y ) { + const tripoint_bub_ms next( n.x(), y, c.z() ); + emit_fn( next ); + } + } + } else if( d.y() != 0 ) { + // Vertical movement. Visit the following states: + // + // * n * + // . c . + // . p . + // + // where * is a state to be visited + // n is the state in movement direction, visited + // p is the parent state (not visited) + // c is the current state (not visited) + // . is not visited + if( min.y() <= n.y() && n.y() <= max.y() ) { + for( int x = min.x(); x <= max.x(); ++x ) { + const tripoint_bub_ms next( x, n.y(), c.z() ); + emit_fn( next ); + } + } + } else { + // We arrived in this state with same x/y, which means + // we got here by traversing a staircase or similar. Need to + // visit all directions. + for( int y = min.y(); y <= max.y(); ++y ) { + for( int x = min.x(); x <= max.x(); ++x ) { + if( x == c.x() && y == c.y() ) { + continue; + } + const tripoint_bub_ms next( x, y, c.z() ); + emit_fn( next ); + } + } + } + + if( settings.allow_stairways() ) { + if( flags.is_set( PathfindingFlag::GoesDown ) ) { + emit_fn( cache_->down_destination( c ) ); + } + if( flags.is_set( PathfindingFlag::GoesUp ) ) { + emit_fn( cache_->up_destination( c ) ); + } + } + if( flags.is_set( PathfindingFlag::RampUp ) ) { + const tripoint_bub_ms above( c.xy(), c.z() + 1 ); + emit_fn( above ); + } + if( flags.is_set( PathfindingFlag::RampDown ) ) { + const tripoint_bub_ms below( c.xy(), c.z() - 1 ); + emit_fn( below ); + } + } ); +} + + +// Legacy Pathfinding. + struct pathfinding_settings { int bash_strength = 0; int max_dist = 0; @@ -77,6 +752,9 @@ struct pathfinding_settings { avoid_rough_terrain( art ), avoid_sharp( as ) {} pathfinding_settings &operator=( const pathfinding_settings & ) = default; + + // Converion to the new settings, while everything is being migrated. + PathfindingSettings to_new_pathfinding_settings() const; }; #endif // CATA_SRC_PATHFINDING_H diff --git a/src/point.cpp b/src/point.cpp index d2ceed95f6758..8008f7385db0f 100644 --- a/src/point.cpp +++ b/src/point.cpp @@ -159,26 +159,10 @@ std::vector closest_points_first( const point ¢er, int min_dist, int std::vector result; result.reserve( n + ( is_center_included ? 1 : 0 ) ); - if( is_center_included ) { - result.push_back( center ); - } - - int x_init = std::max( min_dist, 1 ); - point p( x_init, 1 - x_init ); - - point d( point_east ); - - for( int i = 0; i < n; i++ ) { - result.push_back( center + p ); - - if( p.x == p.y || ( p.x < 0 && p.x == -p.y ) || ( p.x > 0 && p.x == 1 - p.y ) ) { - std::swap( d.x, d.y ); - d.x = -d.x; - } - - p.x += d.x; - p.y += d.y; - } + find_point_closest_first( center, min_dist, max_dist, [&result]( const point & p ) { + result.push_back( p ); + return false; + } ); return result; } diff --git a/src/point.h b/src/point.h index 3047978be41d7..d1a2c6930f6fd 100644 --- a/src/point.h +++ b/src/point.h @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -290,6 +291,11 @@ std::vector closest_points_first( const tripoint ¢er, int min_dist std::vector closest_points_first( const point ¢er, int max_dist ); std::vector closest_points_first( const point ¢er, int min_dist, int max_dist ); +// TODO: Make this into an Input Iterator. +template +std::optional find_point_closest_first( const point ¢er, int min_dist, int max_dist, + PredicateFn &&fn ); + inline constexpr tripoint tripoint_min { INT_MIN, INT_MIN, INT_MIN }; inline constexpr tripoint tripoint_max{ INT_MAX, INT_MAX, INT_MAX }; @@ -363,4 +369,50 @@ inline const std::array eight_horizontal_neighbors = { { } }; +template +std::optional find_point_closest_first( const point ¢er, int min_dist, int max_dist, + PredicateFn &&predicate_fn ) +{ + min_dist = std::max( min_dist, 0 ); + max_dist = std::max( max_dist, 0 ); + + if( min_dist > max_dist ) { + return std::nullopt; + } + + const int min_edge = min_dist * 2 + 1; + const int max_edge = max_dist * 2 + 1; + + const int n = max_edge * max_edge - ( min_edge - 2 ) * ( min_edge - 2 ); + const bool is_center_included = min_dist == 0; + + if( is_center_included ) { + if( predicate_fn( center ) ) { + return center; + } + } + + int x_init = std::max( min_dist, 1 ); + point p( x_init, 1 - x_init ); + + point d( point_east ); + + for( int i = 0; i < n; i++ ) { + const point next = center + p; + if( predicate_fn( next ) ) { + return next; + } + + if( p.x == p.y || ( p.x < 0 && p.x == -p.y ) || ( p.x > 0 && p.x == 1 - p.y ) ) { + std::swap( d.x, d.y ); + d.x = -d.x; + } + + p.x += d.x; + p.y += d.y; + } + + return std::nullopt; +} + #endif // CATA_SRC_POINT_H