diff --git a/data/json/npcs/common_chat/TALK_COMMON_ALLY.json b/data/json/npcs/common_chat/TALK_COMMON_ALLY.json index c2f99bbef60d8..af304796a7379 100644 --- a/data/json/npcs/common_chat/TALK_COMMON_ALLY.json +++ b/data/json/npcs/common_chat/TALK_COMMON_ALLY.json @@ -51,6 +51,7 @@ "topic": "TALK_FRIEND", "effect": "reveal_stats" }, + { "text": "Please go to this location.", "topic": "TALK_GOTO_LOCATION", "effect": "goto_location" }, { "text": "There's something I want you to do.", "topic": "TALK_ALLY_ORDERS" }, { "text": "I just wanted to talk for a bit.", "topic": "TALK_ALLY_SOCIAL" }, { "text": "Can you help me understand something? (HELP/TUTORIAL)", "topic": "TALK_ALLY_TUTORIAL" }, @@ -1017,7 +1018,10 @@ { "id": "TALK_GOTO_LOCATION", "type": "talk_topic", - "dynamic_line": "Sure thing, I'll make my way there.", + "dynamic_line": { + "npc_is_travelling": "Sure thing, I'll make my way there.", + "no": "All right, tell me if you want me to go somewhere." + }, "responses": [ { "text": "Affirmative.", "topic": "TALK_DONE" } ] }, { @@ -1037,12 +1041,6 @@ "topic": "TALK_FRIEND_GUARD", "effect": "assign_camp" }, - { - "text": "Please go to this location.", - "topic": "TALK_GOTO_LOCATION", - "condition": { "or": [ "is_by_radio", "u_has_camp" ] }, - "effect": "goto_location" - }, { "text": "I want you to build a camp here.", "topic": "TALK_HALLU_CAMP", diff --git a/doc/NPCs.md b/doc/NPCs.md index c9493dbf32179..3c78bbcd32762 100644 --- a/doc/NPCs.md +++ b/doc/NPCs.md @@ -961,6 +961,8 @@ Condition | Type | Description `"has_available_mission" or "u_has_available_mission" or "npc_has_available_mission"` | simple string | `true` if u or the NPC has one job available for the player character. `"has_many_available_missions"` | simple string | `true` if the NPC has several jobs available for the player character. `"mission_goal" or "npc_mission_goal" or "u_mission_goal"` | string or [variable object](#variable-object) | `true` if u or the NPC's current mission has the same goal as `mission_goal`. +`"u_has_activity" or "npc_has_activity" | simple string | `true` if the [selected talker](EFFECT_ON_CONDITION.md#alpha-and-beta-talkers) is currently performing an [activity](PLAYER_ACTIVITY.md). +`"u_is_travelling" or "npc_is_travelling" | simple string | `true` if the [selected talker](EFFECT_ON_CONDITION.md#alpha-and-beta-talkers) has a current destination. Note that this just checks the destination exists, not whether u or npc is actively moving to it. `"mission_complete" or "npc_mission_complete" or "u_mission_complete"` | simple string | `true` if u or the NPC has completed the other's current mission. `"mission_incomplete" or "npc_mission_incomplete" or "u_mission_incomplete"` | simple string | `true` if u or the NPC hasn't completed the other's current mission. `"mission_failed" or "npc_mission_failed" or "u_mission_failed"` | simple string | `true` if u or the NPC has failed the other's current mission. diff --git a/lang/string_extractor/parsers/talk_topic.py b/lang/string_extractor/parsers/talk_topic.py index a71ddd922591f..5233c4959ffd4 100644 --- a/lang/string_extractor/parsers/talk_topic.py +++ b/lang/string_extractor/parsers/talk_topic.py @@ -22,6 +22,7 @@ def gender_options(subject): "npc_available", "npc_following", "npc_friend", "npc_hostile", "npc_train_skills", "npc_train_styles", "npc_train_spells", "at_safe_space", "is_day", "npc_has_activity", + "u_is_travelling", "npc_is_travelling", "is_outside", "u_is_outside", "npc_is_outside", "u_has_camp", "u_can_stow_weapon", "npc_can_stow_weapon", "u_can_drop_weapon", "npc_can_drop_weapon", "u_has_weapon", "npc_has_weapon", diff --git a/src/condition.cpp b/src/condition.cpp index e9e306b9aa3a1..655e4c7b3dbbb 100644 --- a/src/condition.cpp +++ b/src/condition.cpp @@ -1099,6 +1099,17 @@ conditional_t::func f_npc_role_nearby( const JsonObject &jo, std::string_view me }; } +conditional_t::func f_npc_is_travelling( bool is_npc ) +{ + return [is_npc]( dialogue const & d ) { + const Character *traveller = d.actor( is_npc )->get_character(); + if( !traveller ) { + return false; + } + return !traveller->omt_path.empty(); + }; +} + conditional_t::func f_npc_allies( const JsonObject &jo, std::string_view member ) { dbl_or_var dov = get_dbl_or_var( jo, member ); @@ -2487,6 +2498,7 @@ std::vector parsers_simple = { {"u_male", "npc_male", &conditional_fun::f_is_male }, {"u_female", "npc_female", &conditional_fun::f_is_female }, + {"u_is_travelling", "npc_is_travelling", &conditional_fun::f_npc_is_travelling }, {"has_no_assigned_mission", &conditional_fun::f_no_assigned_mission }, {"has_assigned_mission", &conditional_fun::f_has_assigned_mission }, {"has_many_assigned_missions", &conditional_fun::f_has_many_assigned_missions }, diff --git a/src/do_turn.cpp b/src/do_turn.cpp index c30b6b130e279..faabb59d44833 100644 --- a/src/do_turn.cpp +++ b/src/do_turn.cpp @@ -654,7 +654,7 @@ bool do_turn() // consider a stripped down cache just for monsters. m.build_map_cache( levz, true ); monmove(); - if( calendar::once_every( 5_minutes ) ) { + if( calendar::once_every( time_between_npc_OM_moves ) ) { overmap_npc_move(); } if( calendar::once_every( 10_seconds ) ) { diff --git a/src/game.h b/src/game.h index 56ea6185b4a94..ce4150f77f266 100644 --- a/src/game.h +++ b/src/game.h @@ -1154,6 +1154,9 @@ class game // show NPC pathfinding on overmap ui bool debug_pathfinding = false; // NOLINT(cata-serialize) + //Ugly kludge to pass info to tile overmaps + npc *follower_path_to_show = nullptr; // NOLINT(cata-serialize) + /* tile overlays */ // Toggle all other overlays off and flip the given overlay on/off. void display_toggle_overlay( action_id ); diff --git a/src/game_constants.h b/src/game_constants.h index 0a4881cccc959..2a82625d81864 100644 --- a/src/game_constants.h +++ b/src/game_constants.h @@ -4,6 +4,7 @@ #include #include +#include "calendar.h" #include "units.h" // Fixed window sizes. @@ -145,6 +146,9 @@ constexpr int BIO_CQB_LEVEL = 5; // Minimum size of a horde to show up on the minimap. constexpr int HORDE_VISIBILITY_SIZE = 3; +// How often a NPC can move one tile on the overmap +constexpr time_duration time_between_npc_OM_moves = 5_minutes; + /** * Average annual temperature in Kelvin used for climate, weather and temperature calculation. * Average New England temperature = 43F/6C rounded to int. diff --git a/src/npctalk_funcs.cpp b/src/npctalk_funcs.cpp index 28ea6454e0528..9dd5c9ddac4ce 100644 --- a/src/npctalk_funcs.cpp +++ b/src/npctalk_funcs.cpp @@ -49,6 +49,7 @@ #include "npctrade.h" #include "output.h" #include "overmap.h" +#include "overmap_ui.h" #include "overmapbuffer.h" #include "pimpl.h" #include "player_activity.h" @@ -372,7 +373,7 @@ void talk_function::goto_location( npc &p ) camps.push_back( temp_camp ); } for( const basecamp *iter : camps ) { - //~ %1$s: camp name, %2$d and %3$d: coordinates + //~ %1$s: camp name, %2$s: coordinates of the camp selection_menu.addentry( i++, true, MENU_AUTOASSIGN, pgettext( "camp", "%1$s at %2$s" ), iter->camp_name(), iter->camp_omt_pos().to_string() ); } @@ -406,6 +407,19 @@ void talk_function::goto_location( npc &p ) add_msg( m_info, _( "That is not a valid destination for %s." ), p.disp_name() ); return; } + g->follower_path_to_show = &p; // Necessary for overmap display in tiles version... + ui::omap::display_npc_path( p.global_omt_location(), p.omt_path ); + g->follower_path_to_show = nullptr; + int tiles_to_travel = p.omt_path.size(); + time_duration ETA = time_between_npc_OM_moves * tiles_to_travel; + ETA = ETA * rng_float( 0.8, 1.2 ); // Add +-20% variance in our estimate + if( !query_yn( + _( "Estimated time to arrival: %1$s \nTiles to travel: %2$s \nIs this path and destination acceptable?" ), + to_string_approx( ETA ), tiles_to_travel ) ) { + p.goal = npc::no_goal_point; + p.omt_path.clear(); + return; + } p.set_mission( NPC_MISSION_TRAVELLING ); p.chatbin.first_topic = p.chatbin.talk_friend_guard; p.guard_pos = std::nullopt; diff --git a/src/overmap_ui.cpp b/src/overmap_ui.cpp index e1a8df4e55d09..95bab8ccae9b8 100644 --- a/src/overmap_ui.cpp +++ b/src/overmap_ui.cpp @@ -538,7 +538,8 @@ static bool get_and_assign_los( int &los, avatar &player_character, const tripoi static void draw_ascii( const catacurses::window &w, const tripoint_abs_omt ¢er, const tripoint_abs_omt &orig, bool blink, bool show_explored, bool /* fast_scroll */, - input_context * /* inp_ctxt */, const draw_data_t &data ) + input_context * /* inp_ctxt */, const draw_data_t &data, + const std::vector &display_path ) { const int om_map_width = OVERMAP_WINDOW_WIDTH; @@ -689,6 +690,11 @@ static void draw_ascii( npc *npc_to_add = npc_to_get.get(); followers.push_back( npc_to_add ); } + if( !display_path.empty() ) { + for( const tripoint_abs_omt &elem : display_path ) { + npc_path_route.insert( elem ); + } + } // get all traveling NPCs for the debug menu to show pathfinding routes. if( g->debug_pathfinding ) { for( auto &elem : overmap_buffer.get_npcs_near_player( 200 ) ) { @@ -1257,7 +1263,8 @@ tiles_redraw_info redraw_info; static void draw( ui_adaptor &ui, const tripoint_abs_omt ¢er, const tripoint_abs_omt &orig, bool blink, bool show_explored, bool fast_scroll, - input_context *inp_ctxt, const draw_data_t &data ) + input_context *inp_ctxt, const draw_data_t &data, + const std::vector &display_path ) { draw_om_sidebar( ui, g->w_omlegend, center, orig, blink, fast_scroll, inp_ctxt, data ); #if defined( TILES ) @@ -1269,7 +1276,8 @@ static void draw( return; } #endif // TILES - draw_ascii( g->w_overmap, center, orig, blink, show_explored, fast_scroll, inp_ctxt, data ); + draw_ascii( g->w_overmap, center, orig, blink, show_explored, fast_scroll, inp_ctxt, data, + display_path ); } static void create_note( const tripoint_abs_omt &curs ) @@ -1773,7 +1781,7 @@ static bool try_travel_to_destination( avatar &player_character, const tripoint_ } static tripoint_abs_omt display( const tripoint_abs_omt &orig, - const draw_data_t &data = draw_data_t() ) + const draw_data_t &data = draw_data_t(), std::vector display_path = {} ) { const int previous_zoom = g->get_zoom(); g->set_zoom( overmap_zoom_level ); @@ -1861,10 +1869,13 @@ static tripoint_abs_omt display( const tripoint_abs_omt &orig, int fast_scroll_offset = get_option( "FAST_SCROLL_OFFSET" ); std::optional mouse_pos; std::chrono::time_point last_blink = std::chrono::steady_clock::now(); + std::chrono::time_point last_advance = std::chrono::steady_clock::now(); + auto display_path_iter = display_path.rbegin(); + std::chrono::milliseconds cursor_advance_time = std::chrono::milliseconds( 0 ); ui.on_redraw( [&]( ui_adaptor & ui ) { draw( ui, curs, orig, uistate.overmap_show_overlays, - show_explored, fast_scroll, &ictxt, data ); + show_explored, fast_scroll, &ictxt, data, display_path ); } ); do { @@ -1880,6 +1891,22 @@ static tripoint_abs_omt display( const tripoint_abs_omt &orig, #else action = ictxt.handle_input( get_option( "BLINK_SPEED" ) ); #endif + if( !display_path.empty() ) { + std::chrono::time_point now = std::chrono::steady_clock::now(); + // We go faster per-tile the more we have to go + cursor_advance_time = std::chrono::milliseconds( 1000 ) / display_path.size(); + cursor_advance_time = std::max( cursor_advance_time, std::chrono::milliseconds( 1 ) ); + if( now > last_advance + cursor_advance_time ) { + if( display_path_iter != display_path.rend() ) { + curs = *display_path_iter; + last_advance = now; + display_path_iter++; + } else if( now > last_advance + cursor_advance_time * 10 ) { + action = "QUIT"; + break; + } + } + } if( const std::optional vec = ictxt.get_direction( action ) ) { int scroll_d = fast_scroll ? fast_scroll_offset : 1; curs += vec->xy() * scroll_d; @@ -2053,6 +2080,12 @@ void ui::omap::display() overmap_ui::display( get_player_character().global_omt_location(), overmap_ui::draw_data_t() ); } +void ui::omap::display_npc_path( tripoint_abs_omt starting_pos, + const std::vector &display_path ) +{ + overmap_ui::display( starting_pos, overmap_ui::draw_data_t(), display_path ); +} + void ui::omap::display_hordes() { overmap_ui::draw_data_t data; diff --git a/src/overmap_ui.h b/src/overmap_ui.h index e3ad1c768d088..15633bd7563bd 100644 --- a/src/overmap_ui.h +++ b/src/overmap_ui.h @@ -31,6 +31,11 @@ namespace omap * Display overmap centered at the player's position. */ void display(); +/** + * Display overmap centered at the given NPC's position and visually move across their intended OMT path. + */ +void display_npc_path( tripoint_abs_omt starting_pos, + const std::vector &display_path ); /** * Display overmap like with @ref display() and display hordes. */ diff --git a/src/sdltiles.cpp b/src/sdltiles.cpp index c452baf562e98..527795dcde135 100644 --- a/src/sdltiles.cpp +++ b/src/sdltiles.cpp @@ -979,6 +979,15 @@ void cata_tiles::draw_om( const point &dest, const tripoint_abs_omt ¢er_abs_ } } + if( g->follower_path_to_show ) { + for( const tripoint_abs_omt &pos : g->follower_path_to_show->omt_path ) { + if( pos.z() == center_abs_omt.z() ) { + draw_from_id_string( "highlight", global_omt_to_draw_position( pos ), 0, 0, lit_level::LIT, + false ); + } + } + } + // only draw in full tiles so it doesn't get cut off const std::optional> mission_arrow = get_mission_arrow( full_om_tile_area, center_abs_omt );