From 610514e5fb1fc14d60b501eadea441b3fedaf6fc Mon Sep 17 00:00:00 2001 From: "Clarence \"Sparr\" Risher" Date: Sun, 27 Oct 2024 23:40:48 -0400 Subject: [PATCH] Overhaul faction mission travel. Also map prompt messages. --- .../overmap_terrain_faction_base.json | 3 +- src/basecamp.cpp | 1 + src/basecamp.h | 14 +- src/computer_session.cpp | 3 +- src/debug_menu.cpp | 6 +- src/do_turn.cpp | 2 +- src/faction_camp.cpp | 630 ++++++++++-------- src/line.cpp | 10 + src/line.h | 1 + src/npc.cpp | 8 +- src/npc.h | 20 +- src/npcmove.cpp | 9 +- src/npctalk.cpp | 2 +- src/npctalk_funcs.cpp | 2 +- src/overmap_ui.cpp | 27 +- src/overmap_ui.h | 9 +- src/overmapbuffer.cpp | 46 +- src/overmapbuffer.h | 4 +- src/recipe_groups.cpp | 5 +- src/recipe_groups.h | 2 +- src/savegame.cpp | 2 + src/savegame_json.cpp | 13 + src/sdltiles.cpp | 14 + src/simple_pathfinding.cpp | 193 +++++- src/simple_pathfinding.h | 4 +- 25 files changed, 678 insertions(+), 352 deletions(-) diff --git a/data/json/overmap/overmap_terrain/overmap_terrain_faction_base.json b/data/json/overmap/overmap_terrain/overmap_terrain_faction_base.json index 6deac0555700b..3ecb445159c6a 100644 --- a/data/json/overmap/overmap_terrain/overmap_terrain_faction_base.json +++ b/data/json/overmap/overmap_terrain/overmap_terrain_faction_base.json @@ -7,7 +7,8 @@ "sym": "+", "color": "white", "see_cost": "none", - "flags": [ "NO_ROTATE", "SOURCE_PEOPLE", "SHOULD_NOT_SPAWN" ] + "flags": [ "NO_ROTATE", "SOURCE_PEOPLE", "SHOULD_NOT_SPAWN" ], + "travel_cost_type": "field" }, { "type": "overmap_terrain", diff --git a/src/basecamp.cpp b/src/basecamp.cpp index bfa2d9bfd7d2c..dae73a40b38f1 100644 --- a/src/basecamp.cpp +++ b/src/basecamp.cpp @@ -93,6 +93,7 @@ static const time_duration work_day_hours_time = work_day_hours * 1_hours; time_duration base_camps::to_workdays( const time_duration &work_time ) { + // logic here is duplicated in reverse in basecamp::time_to_food if( work_time < ( work_day_hours + 1 ) * 1_hours ) { return work_time; } diff --git a/src/basecamp.h b/src/basecamp.h index e61591ddd5972..02da38621a09f 100644 --- a/src/basecamp.h +++ b/src/basecamp.h @@ -250,7 +250,8 @@ class basecamp /// Constructs a new nutrients struct in place and forwards it. Passed argument should be in kilocalories. nutrients camp_food_supply( int change ); /// Calculates raw kcal cost from duration of work and exercise, then forwards it to above - nutrients camp_food_supply( time_duration work, float exertion_level = NO_EXERCISE ); + nutrients camp_food_supply( const time_duration &work, float exertion_level = NO_EXERCISE, + const time_duration &travel_time = 0_hours ); /// Evenly distributes the actual consumed food from a work project to the workers assigned to it void feed_workers( const std::vector> &workers, nutrients food, bool is_player_meal = false ); @@ -268,7 +269,8 @@ class basecamp /// The number of days the current camp supplies lasts at the given exertion level. int camp_food_supply_days( float exertion_level ) const; /// Returns the total charges of food time_duration @ref work costs - int time_to_food( time_duration work, float exertion_level = NO_EXERCISE ) const; + int time_to_food( time_duration total_time, float work_exertion_level = NO_EXERCISE, + time_duration travel_time = 0_hours ) const; /// Changes the faction respect for you by @ref change, returns respect int camp_discipline( int change = 0 ) const; /// Changes the faction opinion for you by @ref change, returns opinion @@ -361,11 +363,13 @@ class basecamp npc_ptr start_mission( const mission_id &miss_id, time_duration duration, bool must_feed, const std::string &desc, bool group, const std::vector &equipment, - const skill_id &skill_tested, int skill_level, float exertion_level ); + const skill_id &skill_tested, int skill_level, + float exertion_level, const time_duration &travel_time = 0_hours ); npc_ptr start_mission( const mission_id &miss_id, time_duration duration, bool must_feed, const std::string &desc, bool group, - const std::vector &equipment, float exertion_level, - const std::map &required_skills = {} ); + const std::vector &equipment, + const std::map &required_skills = {}, + float exertion_level = 1.0f, const time_duration &travel_time = 0_hours ); comp_list start_multi_mission( const mission_id &miss_id, bool must_feed, const std::string &desc, // const std::vector& equipment, // No support for extracting equipment from recipes currently.. diff --git a/src/computer_session.cpp b/src/computer_session.cpp index f40fb65c7ffd6..831d92f569bf8 100644 --- a/src/computer_session.cpp +++ b/src/computer_session.cpp @@ -740,7 +740,8 @@ void computer_session::action_miss_disarm() void computer_session::action_miss_launch() { // Target Acquisition. - const tripoint_abs_omt target( ui::omap::choose_point( 0 ) ); + const tripoint_abs_omt target( ui::omap::choose_point( + _( "Choose a target for the nuclear missile." ), 0 ) ); if( target == overmap::invalid_tripoint ) { add_msg( m_info, _( "Target acquisition canceled." ) ); return; diff --git a/src/debug_menu.cpp b/src/debug_menu.cpp index 70b396b1b3ed9..d31ad98034476 100644 --- a/src/debug_menu.cpp +++ b/src/debug_menu.cpp @@ -1660,7 +1660,8 @@ static void teleport_short() static void teleport_long() { - const tripoint_abs_omt where( ui::omap::choose_point( true ) ); + const tripoint_abs_omt where( ui::omap::choose_point( _( "Choose a teleport destination." ), + true ) ); if( where == overmap::invalid_tripoint ) { return; } @@ -3464,7 +3465,8 @@ static void map_extra() mx_menu.query(); int mx_choice = mx_menu.ret; if( mx_choice >= 0 && mx_choice < static_cast( mx_str.size() ) ) { - const tripoint_abs_omt where_omt( ui::omap::choose_point( true ) ); + const tripoint_abs_omt where_omt( ui::omap::choose_point( + _( "Select location to spawn map extra." ), true ) ); if( where_omt != overmap::invalid_tripoint ) { smallmap mx_map; mx_map.load( where_omt, false ); diff --git a/src/do_turn.cpp b/src/do_turn.cpp index e6022764b54a7..e3c7bda4f7e76 100644 --- a/src/do_turn.cpp +++ b/src/do_turn.cpp @@ -419,7 +419,7 @@ void overmap_npc_move() } if( elem->omt_path.empty() ) { elem->omt_path = overmap_buffer.get_travel_path( elem->global_omt_location(), elem->goal, - overmap_path_params::for_npc() ); + overmap_path_params::for_npc() ).points; if( elem->omt_path.empty() ) { // goal is unreachable, or already reached goal, reset it elem->goal = npc::no_goal_point; } diff --git a/src/faction_camp.cpp b/src/faction_camp.cpp index 63452565e99b4..def4e97aa7e4a 100644 --- a/src/faction_camp.cpp +++ b/src/faction_camp.cpp @@ -65,6 +65,7 @@ #include "recipe_groups.h" #include "requirements.h" #include "rng.h" +#include "simple_pathfinding.h" #include "skill.h" #include "string_formatter.h" #include "string_input_popup.h" @@ -102,6 +103,7 @@ Item_spawn_data_gathering_faction_camp_firewood( "gathering_faction_camp_firewoo static const itype_id itype_duffelbag( "duffelbag" ); static const itype_id itype_fungal_seeds( "fungal_seeds" ); static const itype_id itype_log( "log" ); +static const itype_id itype_stick_long( "stick_long" ); static const itype_id itype_marloss_seed( "marloss_seed" ); static const mongroup_id GROUP_CAMP_HUNTING( "GROUP_CAMP_HUNTING" ); @@ -272,7 +274,7 @@ static bool om_set_hide_site( npc &comp, const tripoint_abs_omt &omt_tgt, const drop_locations &itms_rem = {} ); /** * Opens the overmap so that you can select points for missions or constructions. - * @param omt_pos where your camp is, used for calculating travel distances + * @param omt_pos start position, used for calculating travel distances * @param min_range * @param range max number of OM tiles the user can select * @param possible_om_types requires the user to reselect if the OM picked isn't in the list @@ -285,32 +287,33 @@ static tripoint_abs_omt om_target_tile( const tripoint_abs_omt &omt_pos, int min_range = 1, int range = 1, const std::vector &possible_om_types = {}, ot_match_type match_type = ot_match_type::exact, bool must_see = true, - bool popup_notice = true, - const tripoint_abs_omt &source = tripoint_abs_omt( -999, -999, -999 ), - bool bounce = false ); + const tripoint_abs_omt &source = overmap::invalid_tripoint, + bool bounce = false, const std::optional message = std::nullopt ); static void om_range_mark( const tripoint_abs_omt &origin, int range, bool add_notes = true, const std::string &message = "Y;X: MAX RANGE" ); static void om_line_mark( const tripoint_abs_omt &origin, const tripoint_abs_omt &dest, bool add_notes = true, const std::string &message = "R;X: PATH" ); -static std::vector om_companion_path( +static void om_path_mark( + const std::vector ¬e_pts, bool add_notes = true, + const std::string &message = "R;X: PATH" ); +/** + * Select waypoints and plot a path for a companion to travel + * @param start start point + * @param range_start maximum total distance traveled + * @param bounce does visiting a faction hide site extend the range? +*/ +static pf::simple_path om_companion_path( const tripoint_abs_omt &start, int range_start = 90, bool bounce = true ); /** * Can be used to calculate total trip time for an NPC mission or just the traveling portion. - * Doesn't use the pathingalgorithms yet. - * @param omt_pos start point - * @param omt_tgt target point - * @param work how much time the NPC will stay at the target + * @param journey path they will follow * @param trips how many trips back and forth the NPC will make */ -static time_duration companion_travel_time_calc( const tripoint_abs_omt &pos, - const tripoint_abs_omt &tgt, - time_duration work, int trips = 1, int haulage = 0 ); -static time_duration companion_travel_time_calc( - const std::vector &journey, time_duration work, int trips = 1, - int haulage = 0 ); +static time_duration companion_travel_time_calc( const pf::simple_path &journey, + int trips = 1 ); /// Determines how many trips it takes to move @ref mass and @ref volume of items -/// with @ref carry_mass and @ref carry_volume moved per trip +/// with @ref carry_mass and @ref carry_volume moved per trip, assuming round trips static int om_carry_weight_to_trips( const units::mass &total_mass, const units::volume &total_volume, const npc_ptr &comp = nullptr ); static int om_carry_weight_to_trips( const units::mass &mass, const units::volume &volume, @@ -319,7 +322,7 @@ static int om_carry_weight_to_trips( const units::mass &mass, const units::volum static std::string camp_trip_description( const time_duration &total_time, const time_duration &working_time, const time_duration &travel_time, - int distance, int trips, int need_food ); + int dist_m, int trips, int need_food ); /* * check if a companion survives a random encounter @@ -1410,12 +1413,26 @@ void basecamp::get_available_missions( mission_data &mission_key, map &here ) { if( directions.size() < 8 ) { bool free_non_field_found = false; + bool possible_expansion_found = false; for( const auto &dir : base_camps::all_directions ) { - if( dir.first != base_camps::base_dir && expansions.find( dir.first ) == expansions.end() && - overmap_buffer.ter_existing( omt_pos + dir.first ) != oter_id( "field" ) ) { - free_non_field_found = true; - break; + if( dir.first != base_camps::base_dir && expansions.find( dir.first ) == expansions.end() ) { + const oter_id &omt_ref = overmap_buffer.ter( omt_pos + dir.first ); + if( !free_non_field_found && omt_ref != oter_id( "field" ) ) { + free_non_field_found = true; + } + if( !possible_expansion_found ) { + const std::optional *maybe_args = + overmap_buffer.mapgen_args( omt_pos + dir.first ); + const auto &pos_expansions = recipe_group::get_recipes_by_id( "all_faction_base_expansions", + omt_ref, maybe_args, 1 ); + if( !pos_expansions.empty() ) { + possible_expansion_found = true; + } + } + if( free_non_field_found && possible_expansion_found ) { + break; + } } } @@ -1451,22 +1468,6 @@ void basecamp::get_available_missions( mission_data &mission_key, map &here ) } } - bool possible_expansion_found = false; - - for( const auto &dir : base_camps::all_directions ) { - if( dir.first != base_camps::base_dir && expansions.find( dir.first ) == expansions.end() ) { - const oter_id &omt_ref = overmap_buffer.ter( omt_pos + dir.first ); - const std::optional *maybe_args = overmap_buffer.mapgen_args( - omt_pos + dir.first ); - const auto &pos_expansions = recipe_group::get_recipes_by_id( "all_faction_base_expansions", - omt_ref, maybe_args ); - if( !pos_expansions.empty() ) { - possible_expansion_found = true; - break; - } - } - } - const mission_id miss_id = { Camp_Survey_Expansion, "", {}, base_dir }; comp_list npc_list = get_mission_workers( miss_id ); entry = string_format( _( "Notes:\n" @@ -1514,7 +1515,7 @@ void basecamp::get_available_missions( mission_data &mission_key, map &here ) "> Rotten: 0%%\n" "> Rots in < 2 days: 60%%\n" "> Rots in < 5 days: 80%%\n\n" - "Total faction food stock: %d kcal\nor %d / %d / %d day's rations\n" + "Total faction food stock: %d kcal\nor %d / %d / %d days' rations\n" "where the days is measured for Extra / Moderate / No exercise levels" ), fac()->food_supply.kcal(), camp_food_supply_days( EXTRA_EXERCISE ), camp_food_supply_days( MODERATE_EXERCISE ), camp_food_supply_days( NO_EXERCISE ) ); @@ -1947,6 +1948,12 @@ bool basecamp::handle_mission( const ui_mission_id &miss_id ) break; case Camp_Scouting: + if( miss_id.ret ) { + combat_mission_return( miss_id.id ); + } else { + start_combat_mission( miss_id.id, MODERATE_EXERCISE ); + } + break; case Camp_Combat_Patrol: if( miss_id.ret ) { combat_mission_return( miss_id.id ); @@ -1976,20 +1983,22 @@ bool basecamp::handle_mission( const ui_mission_id &miss_id ) npc_ptr basecamp::start_mission( const mission_id &miss_id, time_duration duration, bool must_feed, const std::string &desc, bool /*group*/, const std::vector &equipment, - const skill_id &skill_tested, int skill_level, float exertion_level ) + const skill_id &skill_tested, int skill_level, + float exertion_level, const time_duration &travel_time ) { std::map required_skills; required_skills[ skill_tested ] = skill_level; - return start_mission( miss_id, duration, must_feed, desc, false, equipment, exertion_level, - required_skills ); + return start_mission( miss_id, duration, must_feed, desc, false, equipment, + required_skills, exertion_level, travel_time ); } npc_ptr basecamp::start_mission( const mission_id &miss_id, time_duration duration, bool must_feed, const std::string &desc, bool /*group*/, - const std::vector &equipment, float exertion_level, - const std::map &required_skills ) + const std::vector &equipment, const std::map &required_skills, + float exertion_level, const time_duration &travel_time ) { - if( must_feed && fac()->food_supply.kcal() < time_to_food( duration, exertion_level ) ) { + if( must_feed && + fac()->food_supply.kcal() < time_to_food( duration, exertion_level, travel_time ) ) { popup( _( "You don't have enough food stored to feed your companion." ) ); return nullptr; } @@ -1997,7 +2006,9 @@ npc_ptr basecamp::start_mission( const mission_id &miss_id, time_duration durati false, equipment, required_skills ); if( comp != nullptr ) { comp->companion_mission_time_ret = calendar::turn + duration; + comp->companion_mission_exertion = exertion_level; if( must_feed ) { + // TODO update spent food after assigning a specific worker to the mission and recalculating travel time feed_workers( *comp.get()->as_character(), camp_food_supply( duration, exertion_level ) ); } if( !equipment.empty() ) { @@ -2081,6 +2092,7 @@ comp_list basecamp::start_multi_mission( const mission_id &miss_id, for( npc_ptr &comp : result ) { comp->companion_mission_time_ret = calendar::turn + work_days; + comp->companion_mission_exertion = making.exertion_level(); } if( must_feed ) { std::vector> work_party; @@ -2444,8 +2456,8 @@ void basecamp::job_assignment_ui() smenu.text = _( "Assign job priority (0 to disable)" ); int count = 0; std::vector job_vec = cur_npc->job.get_prioritised_vector(); - smenu.addentry( count, true, 'C', _( "Clear all priorities" ) ); - count++; + smenu.addentry( count++, true, 'C', _( "Clear all priorities" ) ); + smenu.addentry( count++, true, 'C', _( "Set all priorities" ) ); for( const activity_id &elem : job_vec ) { player_activity test_act = player_activity( elem ); const int priority = cur_npc->job.get_priority_of_job( elem ); @@ -2459,16 +2471,23 @@ void basecamp::job_assignment_ui() } if( smenu.ret == 0 ) { cur_npc->job.clear_all_priorities(); - } else if( smenu.ret > 0 && smenu.ret <= static_cast( job_vec.size() ) ) { - activity_id sel_job = job_vec[size_t( smenu.ret - 1 )]; + } else if( smenu.ret == 1 ) { + const int priority = string_input_popup() + .title( _( "Priority for all jobs " ) ) + .width( 20 ) + .only_digits( true ) + .query_int(); + cur_npc->job.set_all_priorities( priority ); + } else if( smenu.ret > 1 && smenu.ret <= static_cast( job_vec.size() ) + 1 ) { + activity_id sel_job = job_vec[size_t( smenu.ret - 2 )]; player_activity test_act = player_activity( sel_job ); const std::string formatted = string_format( _( "Priority for %s " ), test_act.get_verb() ); - const int amount = string_input_popup() - .title( formatted ) - .width( 20 ) - .only_digits( true ) - .query_int(); - cur_npc->job.set_task_priority( sel_job, amount ); + const int priority = string_input_popup() + .title( formatted ) + .width( 20 ) + .only_digits( true ) + .query_int(); + cur_npc->job.set_task_priority( sel_job, priority ); } else { break; } @@ -2575,40 +2594,55 @@ static void change_cleared_terrain( tripoint_abs_omt forest ) } } +static const std::vector terrains_forest = { "forest", "forest_thick", "forest_trail", "rural_road_forest", "rural_road_turn_forest", "rural_road_turn1_forest", "rural_road_3way_forest", "dirt_road_forest", "dirt_road_3way_forest", "dirt_road_turn_forest", "forest_trail_intersection", "special_forest", "special_forest_thick", "forest_trail_isolated", "forest_trail_end" }; +static const std::vector terrains_field_swamp_forest = [] { + std::vector tmp = terrains_forest; + tmp.push_back( "field" ); + tmp.push_back( "forest_water" ); + return tmp; +}(); + + void basecamp::start_cut_logs( const mission_id &miss_id, float exertion_level ) { - std::vector log_sources = { "forest", "forest_thick", "forest_trail", "rural_road_forest", "rural_road_turn_forest", "rural_road_turn1_forest", "rural_road_3way_forest", - "dirt_road_forest", "dirt_road_3way_forest", "dirt_road_turn_forest", "forest_trail_intersection", "special_forest", "special_forest_thick", "forest_trail_isolated", "forest_trail_end" - }; - popup( _( "Forests are the only valid cutting locations, with forest dirt roads, forest rural roads, and trails being valid as well. Note that it's likely both forest and field roads look exactly the same after having been cleared." ) ); - tripoint_abs_omt forest = om_target_tile( omt_pos, 1, 50, log_sources, ot_match_type::type ); - if( forest != tripoint_abs_omt( -999, -999, -999 ) ) { + tripoint_abs_omt forest = om_target_tile( omt_pos, 1, 50, terrains_forest, ot_match_type::type, + _( "Select a forest (or road/trail) between %d and %d tiles away." ) ); + if( forest != overmap::invalid_tripoint ) { standard_npc sample_npc( "Temp" ); sample_npc.set_fake( true ); - int tree_est = om_cutdown_trees_est( forest, 50 ); - int tree_young_est = om_harvest_ter_est( sample_npc, forest, - ter_t_tree_young, 50 ); - int dist = rl_dist( forest.xy(), omt_pos.xy() ); - //Very roughly what the player does + 6 hours for prep, clean up, breaks + const int tree_est = om_cutdown_trees_est( forest, 50 ); + const int tree_young_est = om_harvest_ter_est( sample_npc, forest, + ter_t_tree_young, 50 ); + pf::simple_path path = overmap_buffer.get_travel_path( omt_pos, forest, + overmap_path_params::for_npc() ); + //Very roughly what the player does + 6 hours for prep, clean up time_duration chop_time = 6_hours + 1_hours * tree_est + 7_minutes * tree_young_est; - int haul_items = 2 * tree_est + 3 * tree_young_est; - time_duration travel_time = companion_travel_time_calc( forest, omt_pos, 0_minutes, - 2, haul_items ); - time_duration work_time = travel_time + chop_time; - if( !query_yn( _( "Trip Estimate:\n%s" ), camp_trip_description( work_time, - chop_time, travel_time, dist, 2, time_to_food( work_time, exertion_level ) ) ) ) { + // approximate average yields + // TODO use bash results from t_tree_young to get a better estimate + const int trips = om_carry_weight_to_trips( + tree_est * itype_log->weight * 7.5 + tree_young_est * itype_stick_long->weight * 2.5, + tree_est * itype_log->volume * 7.5 + tree_young_est * itype_stick_long->volume * 2.5 + ); + const time_duration travel_time = companion_travel_time_calc( path, trips ); + const time_duration total_time = base_camps::to_workdays( travel_time + chop_time ); + const int need_food = time_to_food( total_time, exertion_level, travel_time ); + if( !query_yn( _( "Trip Estimate:\n%s" ), camp_trip_description( total_time, + chop_time, travel_time, path.dist, trips, need_food ) ) ) { return; } npc_ptr comp = start_mission( miss_id, - work_time, true, - _( "departs to cut logs…" ), false, {}, + total_time, true, + _( "departs to cut logs. When they finish the area may look a lot like a field." ), false, {}, skill_fabrication, 2, exertion_level ); if( comp != nullptr ) { om_cutdown_trees_logs( forest, 50 ); om_harvest_ter( *comp, forest, ter_t_tree_young, 50 ); - om_harvest_itm( comp, forest, 95 ); - comp->companion_mission_time_ret = calendar::turn + work_time; + const mass_volume results = om_harvest_itm( comp, forest, 95 ); + const int trips = om_carry_weight_to_trips( results.wgt, results.vol, comp ); + const time_duration travel_time = companion_travel_time_calc( path, trips ); + const time_duration total_time = base_camps::to_workdays( travel_time + chop_time ); + comp->companion_mission_time_ret = calendar::turn + total_time; change_cleared_terrain( forest ); } } @@ -2616,31 +2650,30 @@ void basecamp::start_cut_logs( const mission_id &miss_id, float exertion_level ) void basecamp::start_clearcut( const mission_id &miss_id, float exertion_level ) { - std::vector log_sources = { "forest", "forest_thick", "forest_trail", "rural_road_forest", "rural_road_turn_forest", "rural_road_turn1_forest", "rural_road_3way_forest", - "dirt_road_forest", "dirt_road_3way_forest", "dirt_road_turn_forest", "forest_trail_intersection", "special_forest", "special_forest_thick", "forest_trail_isolated", "forest_trail_end" - }; popup( _( "Forests are the only valid cutting locations, with forest dirt roads, forest rural roads, and trails being valid as well. Note that it's likely both forest and field roads look exactly the same after having been cleared." ) ); - tripoint_abs_omt forest = om_target_tile( omt_pos, 1, 50, log_sources, ot_match_type::type ); - if( forest != tripoint_abs_omt( -999, -999, -999 ) ) { + tripoint_abs_omt forest = om_target_tile( omt_pos, 1, 50, terrains_forest, ot_match_type::type ); + if( forest != overmap::invalid_tripoint ) { standard_npc sample_npc( "Temp" ); sample_npc.set_fake( true ); - int tree_est = om_cutdown_trees_est( forest, 95 ); - int tree_young_est = om_harvest_ter_est( sample_npc, forest, - ter_t_tree_young, 95 ); - int dist = rl_dist( forest.xy(), omt_pos.xy() ); - //Very roughly what the player does + 6 hours for prep, clean up, breaks - time_duration chop_time = 6_hours + 1_hours * tree_est + 7_minutes * tree_young_est; - time_duration travel_time = companion_travel_time_calc( forest, omt_pos, 0_minutes, 2 ); - time_duration work_time = travel_time + chop_time; - if( !query_yn( _( "Trip Estimate:\n%s" ), camp_trip_description( work_time, - chop_time, travel_time, dist, 2, time_to_food( work_time, exertion_level ) ) ) ) { + const int tree_est = om_cutdown_trees_est( forest, 95 ); + const int tree_young_est = om_harvest_ter_est( sample_npc, forest, + ter_t_tree_young, 95 ); + pf::simple_path path = overmap_buffer.get_travel_path( omt_pos, forest, + overmap_path_params::for_npc() ); + //Very roughly what the player does + 6 hours for prep, clean up + const time_duration chop_time = 6_hours + 1_hours * tree_est + 7_minutes * tree_young_est; + const time_duration travel_time = companion_travel_time_calc( path, 2 ); + const time_duration total_time = base_camps::to_workdays( travel_time + chop_time ); + const int need_food = time_to_food( total_time, exertion_level, travel_time ); + if( !query_yn( _( "Trip Estimate:\n%s" ), camp_trip_description( total_time, + chop_time, travel_time, path.dist, 2, need_food ) ) ) { return; } npc_ptr comp = start_mission( miss_id, - work_time, + total_time, true, _( "departs to clear a forest…" ), false, {}, - skill_fabrication, 1, exertion_level ); + skill_fabrication, 1, exertion_level, travel_time ); if( comp != nullptr ) { om_cutdown_trees_trunks( forest, 95 ); om_harvest_ter_break( *comp, forest, ter_t_tree_young, 95 ); @@ -2651,14 +2684,12 @@ void basecamp::start_clearcut( const mission_id &miss_id, float exertion_level ) void basecamp::start_setup_hide_site( const mission_id &miss_id, float exertion_level ) { - std::vector hide_locations = { "forest", "forest_thick", "forest_water", "forest_trail" - "field" - }; - popup( _( "Forests, swamps, and fields are valid hide site locations." ) ); - tripoint_abs_omt forest = om_target_tile( omt_pos, 10, 90, hide_locations, ot_match_type::type, - true, true, omt_pos, true ); - if( forest != tripoint_abs_omt( -999, -999, -999 ) ) { - int dist = rl_dist( forest.xy(), omt_pos.xy() ); + tripoint_abs_omt forest = om_target_tile( omt_pos, 10, 90, terrains_field_swamp_forest, + ot_match_type::type, + true, omt_pos, true, _( "Select an forest, swamp, or field between %d and %d tiles away." ) ); + if( forest != overmap::invalid_tripoint ) { + pf::simple_path path = overmap_buffer.get_travel_path( omt_pos, forest, + overmap_path_params::for_npc() ); Character *pc = &get_player_character(); const inventory_filter_preset preset( []( const item_location & location ) { return !location->can_revive() && !location->will_spill(); @@ -2671,41 +2702,40 @@ void basecamp::start_setup_hide_site( const mission_id &miss_id, float exertion_ _( "These are the items you've selected so far." ), _( "Select items to send" ), total_volume, total_mass ); - int trips = om_carry_weight_to_trips( total_mass, total_volume, nullptr ); - int haulage = trips <= 2 ? 0 : losing_equipment.size(); - time_duration build_time = 6_hours; - time_duration travel_time = companion_travel_time_calc( forest, omt_pos, 0_minutes, - 2, haulage ); - time_duration work_time = travel_time + build_time; - if( !query_yn( _( "Trip Estimate:\n%s" ), camp_trip_description( work_time, - build_time, travel_time, dist, trips, time_to_food( work_time, exertion_level ) ) ) ) { + const int trips = om_carry_weight_to_trips( total_mass, total_volume ); + const time_duration build_time = 6_hours; + const time_duration travel_time = companion_travel_time_calc( path, trips ); + const time_duration total_time = base_camps::to_workdays( travel_time + build_time ); + const int need_food = time_to_food( total_time, exertion_level, travel_time ); + if( !query_yn( _( "Trip Estimate:\n%s" ), camp_trip_description( total_time, + build_time, travel_time, path.dist, trips, need_food ) ) ) { return; } npc_ptr comp = start_mission( miss_id, - work_time, true, + total_time, true, _( "departs to build a hide site…" ), false, {}, - skill_survival, 3, exertion_level ); + skill_survival, 3, exertion_level, travel_time ); if( comp != nullptr ) { - trips = om_carry_weight_to_trips( total_mass, total_volume, comp ); - haulage = trips <= 2 ? 0 : losing_equipment.size(); - work_time = companion_travel_time_calc( forest, omt_pos, 0_minutes, 2, haulage ) + - build_time; - comp->companion_mission_time_ret = calendar::turn + work_time; + const int trips = om_carry_weight_to_trips( total_mass, total_volume, comp ); + const time_duration travel_time = companion_travel_time_calc( path, trips ); + const time_duration total_time = base_camps::to_workdays( travel_time + build_time ); + comp->companion_mission_time_ret = calendar::turn + total_time; om_set_hide_site( *comp, forest, losing_equipment ); } } } static const tripoint_omt_ms relay_site_stash{ 11, 10, 0 }; +static const std::vector hide_locations = { faction_hide_site_0_string }; void basecamp::start_relay_hide_site( const mission_id &miss_id, float exertion_level ) { - std::vector hide_locations = { faction_hide_site_0_string }; - popup( _( "You must select an existing hide site." ) ); tripoint_abs_omt forest = om_target_tile( omt_pos, 10, 90, hide_locations, ot_match_type::exact, - true, true, omt_pos, true ); - if( forest != tripoint_abs_omt( -999, -999, -999 ) ) { - int dist = rl_dist( forest.xy(), omt_pos.xy() ); + true, omt_pos, true, string_format( + _( "Select an existing hide site between %d and %d tiles away." ), 10, 90 ) ); + if( forest != overmap::invalid_tripoint ) { + pf::simple_path path = overmap_buffer.get_travel_path( omt_pos, forest, + overmap_path_params::for_npc() ); Character *pc = &get_player_character(); const inventory_filter_preset preset( []( const item_location & location ) { return !location->can_revive() && !location->will_spill(); @@ -2730,38 +2760,38 @@ void basecamp::start_relay_hide_site( const mission_id &miss_id, float exertion_ total_import_volume, total_import_mass ); if( !losing_equipment.empty() || !gaining_equipment.empty() ) { - //Only get charged the greater trips since return is free for both - int trips = std::max( om_carry_weight_to_trips( total_import_mass, total_import_volume, nullptr ), - om_carry_weight_to_trips( total_export_mass, total_export_volume, nullptr ) ); - int haulage = trips <= 2 ? 0 : std::max( gaining_equipment.size(), - losing_equipment.size() ); - time_duration build_time = - 5_minutes; // We're not actually constructing anything, just loading/unloading/performing very light maintenance - time_duration travel_time = companion_travel_time_calc( forest, omt_pos, 0_minutes, - trips, haulage ); - time_duration work_time = travel_time + build_time; - if( !query_yn( _( "Trip Estimate:\n%s" ), camp_trip_description( work_time, build_time, - travel_time, dist, trips, time_to_food( work_time, exertion_level ) ) ) ) { + // We get the lesser direction for free + const int trips = std::max( + om_carry_weight_to_trips( total_import_mass, total_import_volume ), + om_carry_weight_to_trips( total_export_mass, total_export_volume ) + ); + // We're not actually constructing anything, just loading/unloading/performing very light maintenance + const time_duration build_time = 5_minutes; + const time_duration travel_time = companion_travel_time_calc( path, trips ); + const time_duration total_time = base_camps::to_workdays( travel_time + build_time ); + const int need_food = time_to_food( total_time, exertion_level, travel_time ); + if( !query_yn( _( "Trip Estimate:\n%s" ), camp_trip_description( total_time, build_time, + travel_time, path.dist, trips, need_food ) ) ) { return; } npc_ptr comp = start_mission( miss_id, - work_time, true, + total_time, true, _( "departs for the hide site…" ), false, {}, - skill_survival, 3, exertion_level ); + skill_survival, 3, exertion_level, travel_time ); if( comp != nullptr ) { // recalculate trips based on actual load - trips = std::max( om_carry_weight_to_trips( total_import_mass, total_import_volume, comp ), - om_carry_weight_to_trips( total_export_mass, total_export_volume, comp ) ); - int haulage = trips <= 2 ? 0 : std::max( gaining_equipment.size(), - losing_equipment.size() ); - work_time = companion_travel_time_calc( forest, omt_pos, 0_minutes, trips, - haulage ) + build_time; - comp->companion_mission_time_ret = calendar::turn + work_time; + const int trips = std::max( + om_carry_weight_to_trips( total_import_mass, total_import_volume, comp ), + om_carry_weight_to_trips( total_export_mass, total_export_volume, comp ) + ); + const time_duration travel_time = companion_travel_time_calc( path, trips ); + const time_duration total_time = base_camps::to_workdays( travel_time + build_time ); + comp->companion_mission_time_ret = calendar::turn + total_time; om_set_hide_site( *comp, forest, losing_equipment, gaining_equipment ); } } else { - popup( _( "You need equipment to transport between the hide site…" ) ); + popup( _( "You need equipment to transport to/from the hide site…" ) ); } } } @@ -2825,18 +2855,19 @@ static void apply_fortifications( const mission_id &miss_id, const npc_ptr *comp void basecamp::start_fortifications( const mission_id &miss_id, float exertion_level ) { - std::vector allowed_locations = { - "forest", "forest_thick", "forest_water", "forest_trail", "field" - }; popup( _( "Select a start and end point. Line must be straight. Fields, forests, and " "swamps are valid fortification locations. In addition to existing fortification " "constructions." ) ); - tripoint_abs_omt start = om_target_tile( omt_pos, 2, 90, allowed_locations, ot_match_type::type ); - popup( _( "Select an end point." ) ); - tripoint_abs_omt stop = om_target_tile( omt_pos, 2, 90, allowed_locations, ot_match_type::type, - true, false, start ); - if( start != tripoint_abs_omt( -999, -999, -999 ) && - stop != tripoint_abs_omt( -999, -999, -999 ) ) { + tripoint_abs_omt start = om_target_tile( omt_pos, 2, 90, terrains_field_swamp_forest, + ot_match_type::type, true, omt_pos, _( "Select a start point between %d and %d tiles away." ) ); + if( start == overmap::invalid_tripoint ) { + return; + } + tripoint_abs_omt stop = om_target_tile( omt_pos, 2, 90, terrains_field_swamp_forest, + ot_match_type::type, + true, start, _( "Select an end point between %d and %d tiles away." ) ); + if( start != overmap::invalid_tripoint && + stop != overmap::invalid_tripoint ) { const recipe &making = recipe_id( miss_id.parameters ).obj(); bool change_x = start.x() != stop.x(); bool change_y = start.y() != stop.y(); @@ -2846,6 +2877,8 @@ void basecamp::start_fortifications( const mission_id &miss_id, float exertion_l } if( miss_id.parameters == faction_wall_level_n_1_string ) { std::vector tmp_line = line_to( stop, start ); + // line_to doesn't include the origin point + tmp_line.emplace_back( stop ); int line_count = tmp_line.size(); int yes_count = 0; for( tripoint_abs_omt &elem : tmp_line ) { @@ -2876,7 +2909,7 @@ void basecamp::start_fortifications( const mission_id &miss_id, float exertion_l for( tripoint_abs_omt &fort_om : fortify_om ) { bool valid = false; const oter_id &omt_ref = overmap_buffer.ter( fort_om ); - for( const std::string &pos_om : allowed_locations ) { + for( const std::string &pos_om : terrains_field_swamp_forest ) { if( omt_ref.id().c_str() == pos_om ) { valid = true; break; @@ -2887,13 +2920,33 @@ void basecamp::start_fortifications( const mission_id &miss_id, float exertion_l popup( _( "Invalid terrain in construction path." ) ); return; } - trips += 2; - build_time += making.batch_duration( get_player_character() ); - dist += rl_dist( fort_om.xy(), omt_pos.xy() ); - travel_time += companion_travel_time_calc( fort_om, omt_pos, 0_minutes, 2 ); - } - time_duration total_time = base_camps::to_workdays( travel_time + build_time ); - int need_food = time_to_food( total_time, exertion_level ); + // spiked pit requires traveling back and forth to carry components + // TODO calculate whether one trip can carry multiple tiles worth of components + if( miss_id.parameters == faction_wall_level_n_1_string ) { + trips += 2; + const pf::simple_path &path = overmap_buffer.get_travel_path( omt_pos, fort_om, + overmap_path_params::for_npc() ); + travel_time += companion_travel_time_calc( path, 2 ); + dist += path.dist * 2; + } + build_time += making.batch_duration( get_player_character() ); // TODO calculate for NPC, not player + } + // trench requires just one triangular trip + if( miss_id.parameters != faction_wall_level_n_1_string ) { + trips = 1; + const pf::simple_path &path1 = overmap_buffer.get_travel_path( omt_pos, start, + overmap_path_params::for_npc() ); + const pf::simple_path &path2 = overmap_buffer.get_travel_path( start, stop, + overmap_path_params::for_npc() ); + const pf::simple_path &path3 = overmap_buffer.get_travel_path( stop, omt_pos, + overmap_path_params::for_npc() ); + travel_time = companion_travel_time_calc( path1, 1 ) + + companion_travel_time_calc( path2, 1 ) + + companion_travel_time_calc( path3, 1 ); + dist = path1.dist + path2.dist + path3.dist; + } + const time_duration total_time = base_camps::to_workdays( travel_time + build_time ); + const int need_food = time_to_food( total_time, exertion_level, travel_time ); if( !query_yn( _( "Trip Estimate:\n%s" ), camp_trip_description( total_time, build_time, travel_time, dist, trips, need_food ) ) ) { return; @@ -2913,7 +2966,7 @@ void basecamp::start_fortifications( const mission_id &miss_id, float exertion_l npc_ptr comp = start_mission( miss_id, total_time, true, _( "begins constructing fortifications…" ), false, {}, - exertion_level, making.required_skills ); + making.required_skills, exertion_level ); if( comp != nullptr ) { components.consume_components(); for( tripoint_abs_omt &pt : fortify_om ) { @@ -2936,7 +2989,7 @@ static const double diagonal_salt_pipe_cost = std::sqrt( 2.0 ); static const double salt_pipe_legal = 0.0; static const double salt_pipe_illegal = -0.1; static const double salt_pipe_swamp = -0.2; -static constexpr size_t path_map_size = 2 * max_salt_water_pipe_distance + 1; +static const size_t path_map_size = 2 * max_salt_water_pipe_distance + 1; using PathMap = cata::mdarray; // The logic discourages diagonal connections when there are horizontal ones @@ -3476,17 +3529,24 @@ void basecamp::continue_salt_water_pipe( const mission_id &miss_id ) void basecamp::start_combat_mission( const mission_id &miss_id, float exertion_level ) { popup( _( "Select checkpoints until you reach maximum range or select the last point again " - "to end." ) ); + "to end. Cancel to undo a selection." ) ); tripoint_abs_omt start = omt_pos; - std::vector scout_points = om_companion_path( start, 90, true ); + const pf::simple_path &scout_path = om_companion_path( start, 90, + true ); + const std::vector &scout_points = scout_path.points; if( scout_points.empty() ) { return; } - int dist = scout_points.size(); int trips = 2; - time_duration travel_time = companion_travel_time_calc( scout_points, 0_minutes, trips ); - if( !query_yn( _( "Trip Estimate:\n%s" ), camp_trip_description( 0_hours, 0_hours, - travel_time, dist, trips, time_to_food( travel_time, exertion_level ) ) ) ) { + if( scout_points.front() == start ) { + trips = 1; + } + const time_duration travel_time = companion_travel_time_calc( scout_path, trips ); + const time_duration total_time = base_camps::to_workdays( travel_time ); + // travel time is work time for this mission, different exertion level for scout vs combat + const int need_food = time_to_food( total_time, exertion_level ); + if( !query_yn( _( "Trip Estimate:\n%s" ), camp_trip_description( total_time, 0_hours, + travel_time, scout_path.dist, trips, need_food ) ) ) { return; } npc_ptr comp = start_mission( miss_id, travel_time, true, _( "departs on patrol…" ), @@ -3539,8 +3599,8 @@ void basecamp::start_crafting( const std::string &type, const mission_id &miss_i time_duration work_days = base_camps::to_workdays( making.batch_duration( get_player_character(), batch_size ) ); npc_ptr comp = start_mission( miss_id, work_days, true, - _( "begins to work…" ), false, {}, making.exertion_level(), - making.required_skills ); + _( "begins to work…" ), false, {}, + making.required_skills, making.exertion_level() ); if( comp != nullptr ) { components.consume_components(); for( const item &results : making.create_results( batch_size ) ) { @@ -3789,7 +3849,8 @@ void basecamp::start_farm_op( const point &dir, const mission_id &miss_id, } case farm_ops::plow: work += 5_minutes * plots_cnt; - start_mission( miss_id, work, true, _( "begins plowing the field…" ), false, {}, exertion_level ); + start_mission( miss_id, work, true, _( "begins plowing the field…" ), false, {}, {}, + exertion_level ); break; default: debugmsg( "Farm operations called with no operation" ); @@ -3834,7 +3895,12 @@ void basecamp::finish_return( npc &comp, const bool fixed_time, const std::strin } // Missions that are not fixed_time pay their food costs at the end, instead of up-front. - int need_food = time_to_food( mission_time - reserve_time ); + int need_food = 0; + if( !fixed_time ) { + // Assume the travel time is constant and any unplanned time was non-travel + need_food = time_to_food( mission_time, comp.companion_mission_exertion, + comp.companion_mission_travel_time ); + } if( fac()->food_supply.kcal() < need_food ) { popup( _( "Your companion seems disappointed that your pantry is empty…" ) ); } @@ -3917,6 +3983,7 @@ npc_ptr basecamp::emergency_recall( const mission_id &miss_id ) } const std::string return_msg = _( "responds to the emergency recall…" ); + // FIXME this might charge food for someone who was already fed? finish_return( *comp, false, return_msg, skill_menial.str(), 0, true ); } return comp; @@ -4447,17 +4514,19 @@ void basecamp::combat_mission_return( const mission_id &miss_id ) patrol.push_back( guy ); } for( tripoint_abs_omt &pt : comp->companion_mission_points ) { - const oter_id &omt_ref = overmap_buffer.ter( pt ); - int swim = comp->get_skill_level( skill_swimming ); - if( is_river( omt_ref ) && swim < 2 ) { - if( swim == 0 ) { - popup( _( "Your companion hit a river and didn't know how to swim…" ) ); - } else { - popup( _( "Your companion hit a river and didn't know how to swim well " - "enough to cross…" ) ); - } - break; - } + // // Crossing water is at least temporarily avoided due to the introduction of + // // pathfinding to the mission system. + // const oter_id &omt_ref = overmap_buffer.ter( pt ); + // int swim = comp->get_skill_level( skill_swimming ); + // if( is_river( omt_ref ) && swim < 2 ) { + // if( swim == 0 ) { + // popup( _( "Your companion hit a river and didn't know how to swim…" ) ); + // } else { + // popup( _( "Your companion hit a river and didn't know how to swim well " + // "enough to cross…" ) ); + // } + // break; + // } comp->death_drops = false; bool outcome = talk_function::companion_om_combat_check( patrol, pt, patrolling ); comp->death_drops = true; @@ -4484,8 +4553,8 @@ bool basecamp::survey_field_return( const mission_id &miss_id ) return false; } - popup( _( "Select a tile up to %d tiles away." ), 1 ); - const tripoint_abs_omt where( ui::omap::choose_point() ); + const tripoint_abs_omt where( ui::omap::choose_point( string_format( + _( "Select a tile up to %d tiles away." ), 1 ) ) ); if( where == overmap::invalid_tripoint ) { return false; } @@ -4561,8 +4630,8 @@ bool basecamp::survey_return( const mission_id &miss_id ) return false; } - popup( _( "Select a tile up to %d tiles away." ), 1 ); - const tripoint_abs_omt where( ui::omap::choose_point() ); + const tripoint_abs_omt where( ui::omap::choose_point( string_format( + _( "Select a tile up to %d tiles away." ), 1 ) ) ); if( where == overmap::invalid_tripoint ) { return false; } @@ -4737,9 +4806,8 @@ int basecamp::recipe_batch_max( const recipe &making ) const const int max_checks = 9; for( size_t batch_size = 1000; batch_size > 0; batch_size /= 10 ) { for( int iter = 0; iter < max_checks; iter++ ) { - time_duration work_days = base_camps::to_workdays( making.batch_duration( - get_player_character(), max_batch + batch_size ) ); - int food_req = time_to_food( work_days ); + int food_req = time_to_food( making.batch_duration( get_player_character(), + max_batch + batch_size ) ); bool can_make = making.deduped_requirements().can_make_with_inventory( _inv, making.get_component_filter(), max_batch + batch_size ); if( can_make && fac()->food_supply.kcal() > food_req ) { @@ -4971,32 +5039,30 @@ mass_volume om_harvest_itm( const npc_ptr &comp, const tripoint_abs_omt &omt_tgt tripoint_abs_omt om_target_tile( const tripoint_abs_omt &omt_pos, int min_range, int range, const std::vector &possible_om_types, ot_match_type match_type, bool must_see, - bool popup_notice, const tripoint_abs_omt &source, bool bounce ) + const tripoint_abs_omt &source, bool bounce, const std::optional message ) { bool errors = false; - if( popup_notice ) { - popup( _( "Select a location between %d and %d tiles away." ), min_range, range ); - } - std::vector bounce_locations = { faction_hide_site_0_string }; tripoint_abs_omt where; om_range_mark( omt_pos, range ); om_range_mark( omt_pos, min_range, true, "Y;X: MIN RANGE" ); - if( source == tripoint_abs_omt( -999, -999, -999 ) ) { - where = ui::omap::choose_point(); + const std::string &real_message = string_format( + message ? *message : _( "Select a location between %d and %d tiles away." ), min_range, range ); + if( source == overmap::invalid_tripoint ) { + where = ui::omap::choose_point( real_message ); } else { - where = ui::omap::choose_point( source ); + where = ui::omap::choose_point( real_message, source ); } om_range_mark( omt_pos, range, false ); om_range_mark( omt_pos, min_range, false, "Y;X: MIN RANGE" ); if( where == overmap::invalid_tripoint ) { - return tripoint_abs_omt( -999, -999, -999 ); + return overmap::invalid_tripoint; } int dist = rl_dist( where.xy(), omt_pos.xy() ); if( dist > range || dist < min_range ) { - popup( _( "You must select a target between %d and %d range from the base. Range: %d" ), + popup( _( "You must select a target between %d and %d range from the previous point. Range: %d" ), min_range, range, dist ); errors = true; } @@ -5016,7 +5082,7 @@ tripoint_abs_omt om_target_tile( const tripoint_abs_omt &omt_pos, int min_range, if( query_yn( _( "Do you want to bounce off this location to extend range?" ) ) ) { om_line_mark( omt_pos, omt_tgt ); tripoint_abs_omt dest = - om_target_tile( omt_tgt, 2, range * .75, possible_om_types, match_type, true, false, + om_target_tile( omt_tgt, 2, range * .75, possible_om_types, match_type, true, omt_tgt, true ); om_line_mark( omt_pos, omt_tgt, false ); return dest; @@ -5035,7 +5101,8 @@ tripoint_abs_omt om_target_tile( const tripoint_abs_omt &omt_pos, int min_range, } } - return om_target_tile( omt_pos, min_range, range, possible_om_types, match_type ); + return om_target_tile( omt_pos, min_range, range, possible_om_types, match_type, must_see, + omt_pos ); } void om_range_mark( const tripoint_abs_omt &origin, int range, bool add_notes, @@ -5084,7 +5151,24 @@ void om_line_mark( const tripoint_abs_omt &origin, const tripoint_abs_omt &dest, { std::vector note_pts = line_to( origin, dest ); - for( tripoint_abs_omt &pt : note_pts ) { + for( const tripoint_abs_omt &pt : note_pts ) { + if( add_notes ) { + if( !overmap_buffer.has_note( pt ) ) { + overmap_buffer.add_note( pt, message ); + } + } else { + if( overmap_buffer.has_note( pt ) && overmap_buffer.note( pt ) == message ) { + overmap_buffer.delete_note( pt ); + } + } + } +} + +static void om_path_mark( + const std::vector ¬e_pts, bool add_notes, + const std::string &message ) +{ + for( const tripoint_abs_omt &pt : note_pts ) { if( add_notes ) { if( !overmap_buffer.has_note( pt ) ) { overmap_buffer.add_note( pt, message ); @@ -5097,6 +5181,7 @@ void om_line_mark( const tripoint_abs_omt &origin, const tripoint_abs_omt &dest, } } + bool om_set_hide_site( npc &comp, const tripoint_abs_omt &omt_tgt, const drop_locations &itms, const drop_locations &itms_rem ) @@ -5147,45 +5232,21 @@ bool om_set_hide_site( npc &comp, const tripoint_abs_omt &omt_tgt, } // path and travel time -time_duration companion_travel_time_calc( const tripoint_abs_omt &omt_pos, - const tripoint_abs_omt &omt_tgt, time_duration work, int trips, int haulage ) +static time_duration companion_travel_time_calc( const pf::simple_path &journey, + int trips ) { - std::vector journey = line_to( omt_pos, omt_tgt ); - return companion_travel_time_calc( journey, work, trips, haulage ); -} - -time_duration companion_travel_time_calc( const std::vector &journey, - time_duration work, int trips, int haulage ) -{ - int one_way = 0; - for( const tripoint_abs_omt &om : journey ) { - const oter_id &omt_ref = overmap_buffer.ter( om ); - std::string om_id = omt_ref.id().c_str(); - // Player walks 1 om in roughly 30 seconds - if( om_id == "field" ) { - one_way += 30 + ( 30 + haulage ); - } else if( omt_ref->get_type_id() == oter_type_forest_trail ) { - one_way += 35 + ( 30 + haulage ); - } else if( om_id == "forest_thick" ) { - one_way += 50 + ( 30 + haulage ); - } else if( om_id == "forest_water" ) { - one_way += 60 + ( 30 + haulage ); - } else if( is_river( omt_ref ) ) { - // hauling stuff over a river is slow, because you have to portage most items - one_way += 200 + ( 40 + haulage ); - } else { - one_way += 40 + ( 30 + haulage ); - } - } - return work + one_way * trips * 1_seconds; + return journey.cost * trips * 1_seconds ; } -int om_carry_weight_to_trips( const units::mass &mass, const units::volume &volume, +int om_carry_weight_to_trips( const units::mass &total_mass, const units::volume &total_volume, const units::mass &carry_mass, const units::volume &carry_volume ) { - int trips_m = 1 + mass / carry_mass; - int trips_v = 1 + volume / carry_volume; - // return the number of round trips + // FIXME handle single high volume items correctly + // character can wield up to weight capacity x4, and walk at reduced speed with that single item + // hauling may be appropriate + int trips_m = ( total_mass + carry_mass - 1_milligram ) / carry_mass; + int trips_v = ( total_volume + carry_volume - 1_ml ) / carry_volume; + // return based on round trips return 2 * std::max( trips_m, trips_v ); } @@ -5200,27 +5261,51 @@ int om_carry_weight_to_trips( const units::mass &total_mass, const units::volume return om_carry_weight_to_trips( total_mass, total_volume, max_m, max_v ); } -std::vector om_companion_path( const tripoint_abs_omt &start, int range_start, +pf::simple_path om_companion_path( const tripoint_abs_omt &start, int range_start, bool bounce ) { - std::vector scout_points; + std::vector> scout_segments; tripoint_abs_omt last = start; int range = range_start; int def_range = range_start; - while( range > 3 ) { - tripoint_abs_omt spt = om_target_tile( last, 0, range, {}, ot_match_type::exact, false, true, last, - false ); - if( spt == tripoint_abs_omt( -999, -999, -999 ) ) { - scout_points.clear(); - return scout_points; + while( true ) { + std::optional message; + if( range == 0 ) { + message = _( "Confirm again to finalize the path, or cancel to undo." ); + } + tripoint_abs_omt spt = om_target_tile( last, 0, range, {}, ot_match_type::exact, false, last, + false, message ); + if( spt == overmap::invalid_tripoint ) { + if( scout_segments.empty() ) { + return {}; + } + om_path_mark( scout_segments.back().points, false ); + range += scout_segments.back().cost / 24; + scout_segments.pop_back(); + if( scout_segments.empty() ) { + last = start; + } else { + last = scout_segments.back().points.front(); + } + continue; } if( last == spt ) { break; } - std::vector note_pts = line_to( last, spt ); - scout_points.insert( scout_points.end(), note_pts.begin(), note_pts.end() ); - om_line_mark( last, spt ); - range -= rl_dist( spt.xy(), last.xy() ); + const pf::simple_path ¬e_pts = overmap_buffer.get_travel_path( last, spt, + overmap_path_params::for_npc() ); + if( note_pts.points.empty() ) { + popup( "FIXME EMPTY NOTE_PTS" ); + continue; + } + om_path_mark( note_pts.points ); + if( note_pts.cost / 24 > range ) { + ui::omap::choose_point( _( "This path is too far, continue to undo and try again." ), spt ); + om_path_mark( note_pts.points, false ); + continue; + } + scout_segments.emplace_back( note_pts ); + range -= note_pts.cost / 24; last = spt; const oter_id &omt_ref = overmap_buffer.ter( last ); @@ -5230,10 +5315,18 @@ std::vector om_companion_path( const tripoint_abs_omt &start, def_range = range; } } - for( tripoint_abs_omt &pt : scout_points ) { - om_line_mark( pt, pt, false ); + for( const pf::simple_path &scout_segment : scout_segments ) { + om_path_mark( scout_segment.points, false ); } - return scout_points; + return std::accumulate( scout_segments.begin(), scout_segments.end(), + pf::simple_path(), []( pf::simple_path a, + const pf::simple_path b ) { + a.points.insert( a.points.begin(), b.points.begin(), b.points.end() ); + a.dist += b.dist; + a.cost += b.cost; + return a; + } + ); } // camp utility functions @@ -5400,19 +5493,18 @@ point talk_function::om_simple_dir( const tripoint_abs_omt &omt_pos, // mission descriptions std::string camp_trip_description( const time_duration &total_time, const time_duration &working_time, - const time_duration &travel_time, int distance, int trips, + const time_duration &travel_time, int dist_m, int trips, int need_food ) { std::string entry = "\n"; - //A square is roughly 3 m - int dist_m = distance * SEEX * 2 * 3; + //A square is roughly 1 m if( dist_m > 1000 ) { entry += string_format( _( ">Distance:%15.2f (km)\n" ), dist_m / 1000.0 ); - entry += string_format( _( ">One Way: %15d (trips)\n" ), trips ); + entry += string_format( _( ">One Way: %15d trips\n" ), trips ); entry += string_format( _( ">Covered: %15.2f (km)\n" ), dist_m / 1000.0 * trips ); } else { entry += string_format( _( ">Distance:%15d (m)\n" ), dist_m ); - entry += string_format( _( ">One Way: %15d (trips)\n" ), trips ); + entry += string_format( _( ">One Way: %15d trips\n" ), trips ); entry += string_format( _( ">Covered: %15d (m)\n" ), dist_m * trips ); } entry += string_format( _( ">Travel: %s\n" ), right_justify( to_string( travel_time ), 23 ) ); @@ -5444,7 +5536,7 @@ std::string basecamp::craft_description( const recipe_id &itm ) comp = string_format( _( "Skill used: %s\nDifficulty: %d\n%s\nTime: %s\nCalories per craft: %s\n" ), making.skill_used.obj().name(), making.difficulty, comp, to_string( base_camps::to_workdays( making.batch_duration( get_player_character() ) ) ), - time_to_food( base_camps::to_workdays( making.batch_duration( get_player_character() ) ), + time_to_food( making.batch_duration( get_player_character() ), itm.obj().exertion_level() ) ); return comp; } @@ -5625,9 +5717,10 @@ nutrients basecamp::camp_food_supply( int change ) return camp_food_supply( added ); } -nutrients basecamp::camp_food_supply( time_duration work, float exertion_level ) +nutrients basecamp::camp_food_supply( const time_duration &work, float exertion_level, + const time_duration &travel_time ) { - return camp_food_supply( -time_to_food( work, exertion_level ) ); + return camp_food_supply( -time_to_food( work, exertion_level, travel_time ) ); } void basecamp::feed_workers( const std::vector> &workers, @@ -5710,13 +5803,20 @@ void basecamp::feed_workers( Character &worker, nutrients food, bool is_player_m feed_workers( work_party, std::move( food ), is_player_meal ); } -int basecamp::time_to_food( time_duration work, float exertion_level ) const +int basecamp::time_to_food( time_duration total_time, float work_exertion_level, + time_duration travel_time ) const { - const int days = to_hours( work ) / 24; - const int work_time = days * work_day_hours + to_hours( work ) - days * 24; - - return base_metabolic_rate * ( work_time * exertion_level + days * work_day_rest_hours * NO_EXERCISE - + days * work_day_idle_hours * SLEEP_EXERCISE ) / 24; + // logic here reverses base_camps::to_workdays + const int days = to_days( total_time ); + const time_duration work_and_travel_time = days * work_day_hours * 1_hours + total_time - days * + 24_hours; + const time_duration work_time = work_and_travel_time - travel_time; + + float effective_hours = work_time * work_exertion_level / 1_hours + + travel_time * MODERATE_EXERCISE / 1_hours + + days * work_day_rest_hours * NO_EXERCISE + + days * work_day_idle_hours * SLEEP_EXERCISE ; + return base_metabolic_rate * effective_hours / 24; } item basecamp::make_fake_food( const nutrients &to_use ) const diff --git a/src/line.cpp b/src/line.cpp index 5cb1f7fee16cb..0f5bc07cda17f 100644 --- a/src/line.cpp +++ b/src/line.cpp @@ -284,6 +284,16 @@ float octile_dist_exact( const point &loc1, const point &loc2 ) return d.x + d.y - 2 * mind + mind * M_SQRT2; } +// Lack of vertical diagonal movement means 3d octile dist equals +// 2d octile dist + z dist +// This should only be used for tile level calculations, there is +// no such thing as an om or omt sized z-level +float octile_dist_exact( const tripoint &loc1, const tripoint &loc2 ) +{ + const float dist_2d = octile_dist_exact( point( loc1.x, loc1.y ), point( loc2.x, loc2.y ) ); + return dist_2d + abs( loc1.z - loc2.z ); +} + units::angle atan2( const point &p ) { return units::atan2( p.y, p.x ); diff --git a/src/line.h b/src/line.h index cb06ee1bbb6dd..d1b879654dc4f 100644 --- a/src/line.h +++ b/src/line.h @@ -257,6 +257,7 @@ int manhattan_dist( const point &loc1, const point &loc2 ); // cost sqrt(2) 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 &loc1, const tripoint &loc2 ); // get angle of direction represented by point units::angle atan2( const point & ); diff --git a/src/npc.cpp b/src/npc.cpp index 15e280ae60ca0..1f59ac95f2e48 100644 --- a/src/npc.cpp +++ b/src/npc.cpp @@ -181,6 +181,12 @@ bool job_data::set_task_priority( const activity_id &task, int new_priority ) } return false; } +void job_data::set_all_priorities( int new_priority ) +{ + for( auto &elem : task_priorities ) { + elem.second = new_priority; + } +} void job_data::clear_all_priorities() { for( auto &elem : task_priorities ) { @@ -3477,7 +3483,7 @@ void npc::set_companion_mission( const tripoint_abs_omt &omt_pos, const std::str void npc::reset_companion_mission() { - comp_mission.position = tripoint_abs_omt( -999, -999, -999 ); + comp_mission.position = overmap::invalid_tripoint; reset_miss_id( comp_mission.miss_id ); comp_mission.role_id.clear(); if( comp_mission.destination ) { diff --git a/src/npc.h b/src/npc.h index b264f11c98e0e..86cc4196a2dbd 100644 --- a/src/npc.h +++ b/src/npc.h @@ -161,6 +161,7 @@ class job_data std::unordered_map fetch_history; bool set_task_priority( const activity_id &task, int new_priority ); + void set_all_priorities( int new_priority ); void clear_all_priorities(); bool has_job() const; int get_priority_of_job( const activity_id &req_job ) const; @@ -1358,16 +1359,23 @@ class npc : public Character std::vector path; // Our movement plans - // Personality & other defining characteristics - std::string companion_mission_role_id; //Set mission source or squad leader for a patrol + //Set mission source or squad leader for a patrol + std::string companion_mission_role_id; //Mission leader use to determine item sorting, patrols use for points std::vector companion_mission_points; - time_point companion_mission_time; //When you left for ongoing/repeating missions - time_point - companion_mission_time_ret; //When you are expected to return for calculated/variable mission returns - inventory companion_mission_inv; //Inventory that is added and dropped on mission + //When you left for ongoing/repeating missions + time_point companion_mission_time; + //When you are expected to return for calculated/variable mission returns + time_point companion_mission_time_ret; + //Work exertion level of current mission + float companion_mission_exertion = 1.0f; + //Travel time for the current mission + time_duration companion_mission_travel_time = 0_hours; + //Inventory that is added and dropped on mission + inventory companion_mission_inv; npc_mission mission = NPC_MISSION_NULL; npc_mission previous_mission = NPC_MISSION_NULL; + // Personality & other defining characteristics npc_personality personality; npc_opinion op_of_u; npc_combat_memory_cache mem_combat; diff --git a/src/npcmove.cpp b/src/npcmove.cpp index 74723e433e850..af6d850a184de 100644 --- a/src/npcmove.cpp +++ b/src/npcmove.cpp @@ -4890,7 +4890,8 @@ void npc::set_omt_destination() } omt_path.clear(); if( goal != overmap::invalid_tripoint ) { - omt_path = overmap_buffer.get_travel_path( surface_omt_loc, goal, overmap_path_params::for_npc() ); + omt_path = overmap_buffer.get_travel_path( surface_omt_loc, goal, + overmap_path_params::for_npc() ).points; } if( !omt_path.empty() ) { dest_type = overmap_buffer.ter( goal )->get_type_id().str(); @@ -4901,11 +4902,13 @@ void npc::set_omt_destination() // couldn't find any places to go, so go somewhere. if( goal == overmap::invalid_tripoint || omt_path.empty() ) { goal = surface_omt_loc + point( rng( -90, 90 ), rng( -90, 90 ) ); - omt_path = overmap_buffer.get_travel_path( surface_omt_loc, goal, overmap_path_params::for_npc() ); + omt_path = overmap_buffer.get_travel_path( surface_omt_loc, goal, + overmap_path_params::for_npc() ).points; // try one more time if( omt_path.empty() ) { goal = surface_omt_loc + point( rng( -90, 90 ), rng( -90, 90 ) ); - omt_path = overmap_buffer.get_travel_path( surface_omt_loc, goal, overmap_path_params::for_npc() ); + omt_path = overmap_buffer.get_travel_path( surface_omt_loc, goal, + overmap_path_params::for_npc() ).points; } if( omt_path.empty() ) { goal = no_goal_point; diff --git a/src/npctalk.cpp b/src/npctalk.cpp index 141c6fa7afce0..54fb4b8730466 100644 --- a/src/npctalk.cpp +++ b/src/npctalk.cpp @@ -4444,7 +4444,7 @@ talk_effect_fun_t::func f_npc_goal( const JsonObject &jo, std::string_view membe tripoint_abs_omt destination = mission_util::get_om_terrain_pos( dest_params, d ); guy->goal = destination; guy->omt_path = overmap_buffer.get_travel_path( guy->global_omt_location(), guy->goal, - overmap_path_params::for_npc() ); + overmap_path_params::for_npc() ).points; if( destination == tripoint_abs_omt() || destination == overmap::invalid_tripoint || guy->omt_path.empty() ) { guy->goal = npc::no_goal_point; diff --git a/src/npctalk_funcs.cpp b/src/npctalk_funcs.cpp index 958f58c754e64..9fa1d2960999a 100644 --- a/src/npctalk_funcs.cpp +++ b/src/npctalk_funcs.cpp @@ -403,7 +403,7 @@ void talk_function::goto_location( npc &p ) } p.goal = destination; p.omt_path = overmap_buffer.get_travel_path( p.global_omt_location(), p.goal, - overmap_path_params::for_npc() ); + overmap_path_params::for_npc() ).points; if( destination == tripoint_abs_omt() || destination == overmap::invalid_tripoint || p.omt_path.empty() ) { p.goal = npc::no_goal_point; diff --git a/src/overmap_ui.cpp b/src/overmap_ui.cpp index 5a0af3d78d620..9ccc6ea425bb0 100644 --- a/src/overmap_ui.cpp +++ b/src/overmap_ui.cpp @@ -854,6 +854,10 @@ static void draw_ascii( std::vector> corner_text; + if( !data.message.empty() ) { + corner_text.emplace_back( c_white, data.message ); + } + if( uistate.overmap_show_map_notes ) { const std::string ¬e_text = overmap_buffer.note( cursor_pos ); if( !note_text.empty() ) { @@ -1702,8 +1706,8 @@ static void modify_horde_func( tripoint_abs_omt &curs ) chosen_group.set_interest( new_value ); break; case 1: - popup( _( "Select a target destination for the horde." ) ); - horde_destination = ui::omap::choose_point( true ); + horde_destination = ui::omap::choose_point( _( "Select a target destination for the horde." ), + true ); if( horde_destination == overmap::invalid_tripoint || horde_destination == tripoint_abs_omt_zero ) { break; } @@ -1791,7 +1795,7 @@ static std::vector get_overmap_path_to( const tripoint_abs_omt if( dest == player_omt_pos || dest == start_omt_pos ) { return {}; } else { - return overmap_buffer.get_travel_path( start_omt_pos, dest, params ); + return overmap_buffer.get_travel_path( start_omt_pos, dest, params ).points; } } @@ -2496,30 +2500,27 @@ void ui::omap::display_zones( const tripoint_abs_omt ¢er, const tripoint_abs overmap_ui::display(); } -tripoint_abs_omt ui::omap::choose_point( bool show_debug_info ) +tripoint_abs_omt ui::omap::choose_point( std::string message, bool show_debug_info ) { - g->overmap_data = overmap_ui::overmap_draw_data_t(); - g->overmap_data.origin_pos = get_player_character().global_omt_location(); - g->overmap_data.debug_info = show_debug_info; + return choose_point( message, get_player_character().global_omt_location(), show_debug_info ); return overmap_ui::display(); } -tripoint_abs_omt ui::omap::choose_point( const tripoint_abs_omt &origin, bool show_debug_info ) +tripoint_abs_omt ui::omap::choose_point( std::string message, const tripoint_abs_omt &origin, + bool show_debug_info ) { g->overmap_data = overmap_ui::overmap_draw_data_t(); + g->overmap_data.message = message; g->overmap_data.origin_pos = origin; g->overmap_data.debug_info = show_debug_info; return overmap_ui::display(); } -tripoint_abs_omt ui::omap::choose_point( int z, bool show_debug_info ) +tripoint_abs_omt ui::omap::choose_point( std::string message, int z, bool show_debug_info ) { - g->overmap_data = overmap_ui::overmap_draw_data_t(); - g->overmap_data.debug_info = show_debug_info; tripoint_abs_omt pos = get_player_character().global_omt_location(); pos.z() = z; - g->overmap_data.origin_pos = pos; - return overmap_ui::display(); + return choose_point( message, pos, show_debug_info ); } void ui::omap::setup_cities_menu( uilist &cities_menu, std::vector &cities_container ) diff --git a/src/overmap_ui.h b/src/overmap_ui.h index 77cc0e94d19b9..1c2b65c533bba 100644 --- a/src/overmap_ui.h +++ b/src/overmap_ui.h @@ -75,20 +75,21 @@ void display_editor(); * @returns The absolute coordinates of the chosen point or * invalid_point if canceled with Escape (or similar key). */ -tripoint_abs_omt choose_point( bool show_debug_info = false ); +tripoint_abs_omt choose_point( std::string message = "", bool show_debug_info = false ); /** * Same as above but start at z-level z instead of players * current z-level, x and y are taken from the players position. */ -tripoint_abs_omt choose_point( int z, bool show_debug_info = false ); +tripoint_abs_omt choose_point( std::string message, int z, bool show_debug_info = false ); /** * Interactive point choosing; used as the map screen. * The map is initially centered on the @ref origin. * @returns The absolute coordinates of the chosen point or * invalid_point if canceled with Escape (or similar key). */ -tripoint_abs_omt choose_point( const tripoint_abs_omt &origin, bool show_debug_info = false ); +tripoint_abs_omt choose_point( std::string message, const tripoint_abs_omt &origin, + bool show_debug_info = false ); void setup_cities_menu( uilist &cities_menu, std::vector &cities_container ); @@ -114,6 +115,8 @@ struct overmap_draw_data_t { bool show_explored = true; // currently fast traveling bool fast_traveling = false; + // message to display while using the map + std::string message = ""; // draw zone location. tripoint_abs_omt select = tripoint_abs_omt( -1, -1, -1 ); diff --git a/src/overmapbuffer.cpp b/src/overmapbuffer.cpp index 3485195934ed0..dca51e1ca791a 100644 --- a/src/overmapbuffer.cpp +++ b/src/overmapbuffer.cpp @@ -869,14 +869,16 @@ bool overmapbuffer::reveal( const tripoint_abs_omt ¢er, int radius, overmap_path_params overmap_path_params::for_player() { overmap_path_params ret; - ret.set_cost( oter_travel_cost_type::road, 10 ); - ret.set_cost( oter_travel_cost_type::dirt_road, 10 ); - ret.set_cost( oter_travel_cost_type::field, 15 ); - ret.set_cost( oter_travel_cost_type::trail, 18 ); - ret.set_cost( oter_travel_cost_type::shore, 20 ); - ret.set_cost( oter_travel_cost_type::forest, 30 ); - ret.set_cost( oter_travel_cost_type::swamp, 100 ); - ret.set_cost( oter_travel_cost_type::other, 30 ); + // 24 tiles = 24 seconds walking + ret.set_cost( oter_travel_cost_type::road, 24 ); + ret.set_cost( oter_travel_cost_type::dirt_road, 24 ); + ret.set_cost( oter_travel_cost_type::field, 36 ); + ret.set_cost( oter_travel_cost_type::trail, 43 ); + ret.set_cost( oter_travel_cost_type::shore, 48 ); + ret.set_cost( oter_travel_cost_type::forest, 72 ); + ret.set_cost( oter_travel_cost_type::swamp, 240 ); + ret.set_cost( oter_travel_cost_type::other, 72 ); + ret.allow_diagonal = true; return ret; } @@ -893,12 +895,12 @@ overmap_path_params overmap_path_params::for_land_vehicle( float offroad_coeff, { const bool can_offroad = offroad_coeff >= 0.05; overmap_path_params ret; - ret.set_cost( oter_travel_cost_type::road, 10 ); - const int field_cost = can_offroad ? std::lround( 15 / std::min( 1.0f, offroad_coeff ) ) : -1; + ret.set_cost( oter_travel_cost_type::road, 8 ); // limited by vehicle autodrive speed + const int field_cost = can_offroad ? std::lround( 12 / std::min( 1.0f, offroad_coeff ) ) : -1; ret.set_cost( oter_travel_cost_type::field, field_cost ); ret.set_cost( oter_travel_cost_type::dirt_road, field_cost ); ret.set_cost( oter_travel_cost_type::trail, - ( can_offroad && tiny ) ? field_cost + 10 : -1 ); + ( can_offroad && tiny ) ? field_cost + 8 : -1 ); if( amphibious ) { const overmap_path_params boat_params = overmap_path_params::for_watercraft(); ret.set_cost( oter_travel_cost_type::water, boat_params.get_cost( oter_travel_cost_type::water ) ); @@ -910,15 +912,15 @@ overmap_path_params overmap_path_params::for_land_vehicle( float offroad_coeff, overmap_path_params overmap_path_params::for_watercraft() { overmap_path_params ret; - ret.set_cost( oter_travel_cost_type::water, 10 ); - ret.set_cost( oter_travel_cost_type::shore, 20 ); + ret.set_cost( oter_travel_cost_type::water, 8 ); // limited by vehicle autodrive speed + ret.set_cost( oter_travel_cost_type::shore, 16 ); return ret; } overmap_path_params overmap_path_params::for_aircraft() { overmap_path_params ret; - ret.set_cost( oter_travel_cost_type::air, 10 ); + ret.set_cost( oter_travel_cost_type::air, 8 ); // limited by vehicle autodrive speed return ret; } @@ -942,7 +944,7 @@ static bool is_ramp( const tripoint_abs_omt &omt_pos ) ( oter->get_type_id() == oter_type_bridgehead_ramp ); } -std::vector overmapbuffer::get_travel_path( +pf::simple_path overmapbuffer::get_travel_path( const tripoint_abs_omt &src, const tripoint_abs_omt &dest, const overmap_path_params ¶ms ) { if( src == overmap::invalid_tripoint || dest == overmap::invalid_tripoint ) { @@ -950,17 +952,21 @@ std::vector overmapbuffer::get_travel_path( } const pf::omt_scoring_fn estimate = [&]( tripoint_abs_omt pos ) { - const int cur_cost = pos == src ? 0 : get_terrain_cost( pos, params ); + int cur_cost = get_terrain_cost( pos, params ); if( cur_cost < 0 ) { - return pf::omt_score::rejected; + if( pos == src ) { + cur_cost = 0; + } else { + return pf::omt_score::rejected; + } } return pf::omt_score( cur_cost, is_ramp( pos ) ); }; constexpr int radius = 4 * OMAPX; // radius of search in OMTs = 4 overmaps - const pf::simple_path path = pf::find_overmap_path( src, dest, radius, estimate, - g->display_om_pathfinding_progress ); - return path.points; + const pf::simple_path &path = pf::find_overmap_path( src, dest, radius, estimate, + g->display_om_pathfinding_progress, std::nullopt, params.allow_diagonal ); + return path; } bool overmapbuffer::reveal_route( const tripoint_abs_omt &source, const tripoint_abs_omt &dest, diff --git a/src/overmapbuffer.h b/src/overmapbuffer.h index c495804f866d7..6fab54e7fec67 100644 --- a/src/overmapbuffer.h +++ b/src/overmapbuffer.h @@ -20,6 +20,7 @@ #include "memory_fast.h" #include "omdata.h" #include "overmap_types.h" +#include "simple_pathfinding.h" #include "type_id.h" class basecamp; @@ -42,6 +43,7 @@ struct overmap_path_params { std::map travel_cost_per_type; bool avoid_danger = true; bool only_known_by_player = true; + bool allow_diagonal = false; void set_cost( const oter_travel_cost_type &type, int v ) { travel_cost_per_type.emplace( type, v ); @@ -360,7 +362,7 @@ class overmapbuffer bool reveal( const tripoint_abs_omt ¢er, int radius ); bool reveal( const tripoint_abs_omt ¢er, int radius, const std::function &filter ); - std::vector get_travel_path( + pf::simple_path get_travel_path( const tripoint_abs_omt &src, const tripoint_abs_omt &dest, const overmap_path_params ¶ms ); bool reveal_route( const tripoint_abs_omt &source, const tripoint_abs_omt &dest, int radius = 0, bool road_only = false ); diff --git a/src/recipe_groups.cpp b/src/recipe_groups.cpp index 8789bfbf88eee..16578490cceb6 100644 --- a/src/recipe_groups.cpp +++ b/src/recipe_groups.cpp @@ -112,7 +112,7 @@ std::map recipe_group::get_recipes_by_id( const std::str } std::map recipe_group::get_recipes_by_id( const std::string &id, - const oter_id &omt_ter, const std::optional *maybe_args ) + const oter_id &omt_ter, const std::optional *maybe_args, const size_t limit ) { std::map all_rec; if( !recipe_groups_data.is_valid( group_id( id ) ) ) { @@ -120,6 +120,9 @@ std::map recipe_group::get_recipes_by_id( const std::str } const recipe_group_data &group = recipe_groups_data.obj( group_id( id ) ); for( const auto &recp : group.recipes ) { + if( limit && all_rec.size() >= limit ) { + break; + } const auto &recp_terrain_it = group.om_terrains.find( recp.first ); if( recp_terrain_it == group.om_terrains.end() ) { debugmsg( "Recipe %s doesn't specify 'om_terrains', use ANY instead if intended to work anywhere", diff --git a/src/recipe_groups.h b/src/recipe_groups.h index 663b17c8c2dbb..9c8d59e84138a 100644 --- a/src/recipe_groups.h +++ b/src/recipe_groups.h @@ -21,7 +21,7 @@ void reset(); std::map get_recipes_by_bldg( const std::string &bldg ); std::map get_recipes_by_id( const std::string &id ); std::map get_recipes_by_id( const std::string &id, const oter_id &omt_ter, - const std::optional *maybe_args ); + const std::optional *maybe_args, size_t limit = 0 ); std::string get_building_of_recipe( const std::string &recipe ); } // namespace recipe_group diff --git a/src/savegame.cpp b/src/savegame.cpp index 014ca9bdbc81a..200f60a48be04 100644 --- a/src/savegame.cpp +++ b/src/savegame.cpp @@ -1838,6 +1838,8 @@ void npc::import_and_clean( const JsonObject &data ) companion_mission_points = defaults.companion_mission_points; companion_mission_time = defaults.companion_mission_time; companion_mission_time_ret = defaults.companion_mission_time_ret; + companion_mission_exertion = defaults.companion_mission_exertion; + companion_mission_travel_time = defaults.companion_mission_travel_time; companion_mission_inv.clear(); chatbin.missions.clear(); chatbin.missions_assigned.clear(); diff --git a/src/savegame_json.cpp b/src/savegame_json.cpp index 121994444654e..1726ef4c9ae16 100644 --- a/src/savegame_json.cpp +++ b/src/savegame_json.cpp @@ -2053,10 +2053,13 @@ void npc::load( const JsonObject &data ) int atttmp = 0; std::string facID; std::string comp_miss_role; + time_duration comp_miss_travel_time; tripoint_abs_omt comp_miss_pt; std::string companion_mission_role; time_point companion_mission_t = calendar::turn_zero; time_point companion_mission_t_r = calendar::turn_zero; + float companion_mission_e = 1.0f; + time_duration companion_mission_t_t; std::string act_id; data.read( "marked_for_death", marked_for_death ); @@ -2173,6 +2176,14 @@ void npc::load( const JsonObject &data ) companion_mission_time_ret = companion_mission_t_r; } + if( data.read( "companion_mission_exertion", companion_mission_e ) ) { + companion_mission_exertion = companion_mission_e; + } + + if( data.read( "comp_mission_travel_time", companion_mission_t_t ) ) { + companion_mission_travel_time = companion_mission_t_t; + } + companion_mission_inv.clear(); if( data.has_member( "companion_mission_inv" ) ) { companion_mission_inv.json_load_items( data.get_member( "companion_mission_inv" ) ); @@ -2264,6 +2275,8 @@ void npc::store( JsonOut &json ) const json.member( "companion_mission_points", companion_mission_points ); json.member( "companion_mission_time", companion_mission_time ); json.member( "companion_mission_time_ret", companion_mission_time_ret ); + json.member( "companion_mission_exertion", companion_mission_exertion ); + json.member( "companion_mission_travel_time", companion_mission_travel_time ); json.member( "companion_mission_inv" ); companion_mission_inv.json_save_items( json ); json.member( "restock", restock ); diff --git a/src/sdltiles.cpp b/src/sdltiles.cpp index 9a9a8f0e3706d..944ba29b0c71f 100644 --- a/src/sdltiles.cpp +++ b/src/sdltiles.cpp @@ -1108,6 +1108,20 @@ void cata_tiles::draw_om( const point &dest, const tripoint_abs_omt ¢er_abs_ notes_window_text.emplace_back( c_white, v.name ); } + if( ! g->overmap_data.message.empty() ) { + const int padding = 2; + SDL_Rect message_background_rect = { + 0, + 0, + fontwidth * utf8_width( g->overmap_data.message ) + padding * 2, + fontheight + padding * 2 + }; + geometry->rect( renderer, message_background_rect, SDL_Color{ 0, 0, 0, 175 } ); + draw_string( *font, renderer, geometry, g->overmap_data.message, point( padding, padding ), + cata_cursesport::colorpairs[c_white.to_color_pair_index()].FG ); + } + + if( !notes_window_text.empty() && !fast_traveling ) { constexpr int padding = 2; diff --git a/src/simple_pathfinding.cpp b/src/simple_pathfinding.cpp index 85bb32109e28d..371e1b796a70e 100644 --- a/src/simple_pathfinding.cpp +++ b/src/simple_pathfinding.cpp @@ -134,12 +134,20 @@ namespace const tripoint &direction_to_tripoint( direction dir ) { switch( dir ) { + case direction::NORTHEAST: + return tripoint_north_east; case direction::EAST: return tripoint_east; + case direction::SOUTHEAST: + return tripoint_south_east; case direction::SOUTH: return tripoint_south; + case direction::SOUTHWEST: + return tripoint_south_west; case direction::WEST: return tripoint_west; + case direction::NORTHWEST: + return tripoint_north_west; case direction::NORTH: return tripoint_north; case direction::ABOVECENTER: @@ -152,7 +160,7 @@ const tripoint &direction_to_tripoint( direction dir ) } } -bool is_horizontal( direction dir ) +bool is_cardinal( direction dir ) { switch( dir ) { case direction::EAST: @@ -233,20 +241,41 @@ struct navigation_node { } }; -const std::vector &enumerate_directions( bool allow_z_change ) +const std::vector &enumerate_directions( bool allow_z_change, bool allow_diagonal ) { - static const std::vector cardinal_dirs = {direction::EAST, direction::SOUTH, direction::WEST, direction::NORTH}; - static const std::vector all_dirs = [&]() { + static const std::vector cardinal_dirs = { direction::EAST, direction::SOUTH, direction::WEST, direction::NORTH }; + static const std::vector vertical_dirs = { direction::ABOVECENTER, direction::BELOWCENTER }; + static const std::vector diagonal_dirs = { direction::NORTHEAST, direction::SOUTHEAST, direction::SOUTHWEST, direction::NORTHWEST }; + static const std::vector horizontal_dirs = [&]() { + std::vector ret = cardinal_dirs; + ret.insert( ret.end(), diagonal_dirs.begin(), diagonal_dirs.end() ); + return ret; + } + (); + static const std::vector orthogonal_dirs = [&]() { std::vector ret = cardinal_dirs; - ret.push_back( direction::ABOVECENTER ); - ret.push_back( direction::BELOWCENTER ); + ret.insert( ret.end(), vertical_dirs.begin(), vertical_dirs.end() ); + return ret; + } + (); + static const std::vector all_dirs = [&]() { + std::vector ret = horizontal_dirs; + ret.insert( ret.end(), vertical_dirs.begin(), vertical_dirs.end() ); return ret; } (); if( allow_z_change ) { - return all_dirs; + if( allow_diagonal ) { + return all_dirs; + } else { + return orthogonal_dirs; + } } else { - return cardinal_dirs; + if( allow_diagonal ) { + return horizontal_dirs; + } else { + return cardinal_dirs; + } } } @@ -255,16 +284,116 @@ direction reverse_direction( direction dir ) return direction_from( -direction_to_tripoint( dir ) ); } -int adjust_omt_cost( int base_cost, direction dir_in, direction dir_out ) +// rounded +inline int cost_half( const int base_cost ) { - // Adjust cost for 90-degree turns. We travel from the midpoint of one edge - // to the midpoint of an adjacent edge in a square, which is a diagonal - // line with length = sqrt(2) / 2 for a unit square. - if( dir_in != dir_out && is_horizontal( dir_in ) && is_horizontal( dir_out ) ) { - // Note: sqrt(2) is approximately equal to 99 / 70. - return base_cost * 99 / 140; + return ( base_cost + 1 ) / 2; +} + +// rounded +inline int cost_z( const int base_cost ) +{ + // assumes Z travel is 1/6 the cost of horizontal travel + return ( base_cost + 3 ) / 6; +} + +// rounded +inline int cost_z_half( const int base_cost ) +{ + // assumes Z travel is 1/6 the cost of horizontal travel + return ( base_cost + 6 ) / 12; +} + +// rounded +inline int cost_diagonal( const int base_cost ) +{ + // sqrt(2) ~= 99 / 70 + return ( base_cost * 99 + 35 ) / 70; +} + +// rounded +inline int cost_diagonal_half( const int base_cost ) +{ + // sqrt(2) ~= 99 / 70 + return ( base_cost * 99 + 70 ) / 140; +} + +// Calculate the cost to cross an OMT based on entry and exit directions +// TODO: memoize the results +int omt_cost_to_cross( int base_cost, direction dir_in, direction dir_out ) +{ + + // Assumptions: + // dir_out != CENTER, although that is conceptually valid + // dir_in != dir_out, which the pathfinder should prevent + + if( dir_in == direction::CENTER || dir_in == direction::ABOVECENTER || + dir_in == direction::BELOWCENTER || dir_out == direction::ABOVECENTER || + dir_out == direction::BELOWCENTER ) { + // some Z travel involved + if( ( dir_in == direction::CENTER || dir_in == direction::ABOVECENTER || + dir_in == direction::BELOWCENTER ) && ( dir_out == direction::ABOVECENTER || + dir_out == direction::BELOWCENTER ) ) { + if( dir_in == direction::CENTER ) { + // center to vertical + return cost_z_half( base_cost ); + } + // vertical to vertical + return cost_z( base_cost ); + } + if( dir_in == direction::ABOVECENTER || dir_in == direction::BELOWCENTER ) { + if( is_cardinal( dir_out ) ) { + // vertical to edge + return cost_z_half( base_cost ) + cost_half( base_cost ); + } + // vertical to corner + return cost_z_half( base_cost ) + cost_diagonal_half( base_cost ); + } + if( dir_out == direction::ABOVECENTER || dir_out == direction::BELOWCENTER ) { + if( is_cardinal( dir_in ) ) { + // edge to vertical + return cost_half( base_cost ) + cost_z_half( base_cost ); + } + // corner to vertical + return cost_diagonal_half( base_cost ) + cost_z_half( base_cost ); + } + if( is_cardinal( dir_out ) ) { + // center to edge + return cost_half( base_cost ); + } + // center to corner + return cost_diagonal_half( base_cost ); + } + // this crossing does not start or end at the center or vertical + if( is_cardinal( dir_in ) && is_cardinal( dir_out ) ) { + if( dir_in == reverse_direction( dir_out ) ) { + return base_cost; // directly across + } + // edge to adjacent edge + return cost_diagonal_half( base_cost ); + } + if( dir_in == reverse_direction( dir_out ) ) { + return cost_diagonal( base_cost ); // directly across diagonally } - return base_cost; + if( !is_cardinal( dir_in ) && !is_cardinal( dir_out ) ) { + return base_cost; // corner to adjacent corner + } + // One of two remaining cases is travel between an edge and an adjacent + // corner. The cost for that case would be base_cost / 2. + // However, the pathfinder won't ever choose it. An orthogonal move from + // the previous node would be shorter. So it's safe to over-estimate the + // cost for that case. + // This logic should be updated if the pathfinder is ever updated with any + // possibility to avoid travel between otherwise-navigable tiles. + // This is forunate, because there's no cheap way to distinguish that case + // from the final case, travel between an edge and a far corner. + return base_cost * 161 / 144; // sqrt5 ~= 161 / 72 + // This is the expensive alternative that can handle both of the final two + // cases. Actually any non-vertical-travel case, but we do the logic tree + // above to avoid needing to do the conversions and math required here. + // It requires direction_to_point similar to direction_to_tripoint + // return base_cost * octile_dist_exact( direction_to_point( dir_in ), direction_to_point( dir_out ) ) / 2; + } } // namespace @@ -276,7 +405,8 @@ omt_score::omt_score( int node_cost, bool allow_z_change ) : node_cost( node_cos simple_path find_overmap_path( const tripoint_abs_omt &source, const tripoint_abs_omt &dest, const int radius, const omt_scoring_fn &scorer, - const std::function &progress_fn, const std::optional &max_cost ) + const std::function &progress_fn, const std::optional &max_cost, + bool allow_diagonal ) { cata_assert( progress_fn != nullptr ); simple_path ret; @@ -288,7 +418,7 @@ simple_path find_overmap_path( const tripoint_abs_omt &source, std::unordered_map known_nodes; std::priority_queue, std::greater<>> open_set; const node_address start( tripoint_zero ); - known_nodes.emplace( start, navigation_node{0, 0, -1, start_score.allow_z_change} ); + known_nodes.emplace( start, navigation_node{0, static_cast( start_score.node_cost ), static_cast( direction::CENTER ), start_score.allow_z_change} ); open_set.push( scored_address{ start, 0 } ); const point_abs_omt source_point = source.xy(); constexpr int max_search_count = 100000; @@ -308,25 +438,37 @@ simple_path find_overmap_path( const tripoint_abs_omt &source, } } const tripoint_abs_omt cur_point = cur_addr.to_tripoint( source ); + const navigation_node &cur_node = known_nodes.at( cur_addr ); if( cur_point == dest ) { + ret.dist = omt_cost_to_cross( 24, direction::CENTER, cur_node.get_prev_dir() ); node_address addr = cur_addr; + const navigation_node *next_node = nullptr; while( !( addr == start ) ) { const navigation_node &node = known_nodes.at( addr ); + if( next_node != nullptr ) { + ret.dist += omt_cost_to_cross( 24, node.get_prev_dir(), + reverse_direction( next_node->get_prev_dir() ) ); + } + next_node = &node; ret.points.emplace_back( addr.to_tripoint( source ) ); addr = addr.displace( node.get_prev_dir() ); } ret.points.emplace_back( addr.to_tripoint( source ) ); + ret.dist += omt_cost_to_cross( 24, direction::CENTER, + next_node->get_prev_dir() ); // this direction is reversed but that doesn't change the result + // total path cost is the cost to reach an edge of the final node plus the cost to reach the center of that node + ret.cost = cur_node.cumulative_cost + omt_cost_to_cross( cur_node.node_cost, direction::CENTER, + cur_node.get_prev_dir() ); return ret; } - const navigation_node &cur_node = known_nodes.at( cur_addr ); - for( direction dir : enumerate_directions( cur_node.allow_z_change ) ) { + for( direction dir : enumerate_directions( cur_node.allow_z_change, allow_diagonal ) ) { if( dir == cur_node.prev_dir ) { continue; // don't go back the way we just came } const direction rev_dir = reverse_direction( dir ); const node_address next_addr = cur_addr.displace( dir ); - const int cumulative_cost = cur_node.cumulative_cost + adjust_omt_cost( cur_node.node_cost, rev_dir, - cur_node.get_prev_dir() ); + const int cumulative_cost = cur_node.cumulative_cost + omt_cost_to_cross( cur_node.node_cost, + cur_node.get_prev_dir(), dir ); auto iter = known_nodes.find( next_addr ); if( iter != known_nodes.end() ) { navigation_node &next_node = iter->second; @@ -344,9 +486,10 @@ simple_path find_overmap_path( const tripoint_abs_omt &source, // TODO: add to closed set to avoid re-visiting continue; } - // TODO: pass in the 10 (default terrain cost) - const int xy_score = octile_dist( next_point.xy(), dest.xy(), 10 ); - const int z_score = std::abs( next_point.z() - dest.z() ) * 10; + // TODO: pass in the 24 (default terrain cost) + const int xy_score = octile_dist( next_point.xy(), dest.xy(), 24 ); + const int z_score = std::abs( next_point.z() - dest.z() ) * + 4; // Z travel is much faster than X/Y travel const int estimated_total_cost = cumulative_cost + next_score.node_cost + xy_score + z_score; if( max_cost && estimated_total_cost > *max_cost ) { continue; diff --git a/src/simple_pathfinding.h b/src/simple_pathfinding.h index ebb324a2c7243..f803cc5dc489e 100644 --- a/src/simple_pathfinding.h +++ b/src/simple_pathfinding.h @@ -44,6 +44,8 @@ struct directed_path { template struct simple_path { std::vector points; + int dist; + int cost; }; // Data structure returned by a node scoring function. @@ -128,7 +130,7 @@ using omt_scoring_fn = std::function; simple_path find_overmap_path( const tripoint_abs_omt &source, const tripoint_abs_omt &dest, int radius, const omt_scoring_fn &scorer, const std::function &progress_fn, - const std::optional &max_cost = std::nullopt ); + const std::optional &max_cost = std::nullopt, bool allow_diagonal = false ); } // namespace pf