From b8de9f9850c52064e7684ae60ed224a6ebb1290d Mon Sep 17 00:00:00 2001 From: eso Date: Sun, 12 May 2019 01:28:07 -0700 Subject: [PATCH] Refine the yell (C) menu for interactions with NPCs and follower orders (#30358) * follower orders menu refinement --- data/json/npcs/TALK_COMMON_ALLY.json | 76 +++--- data/json/npcs/talk_tags.json | 155 ++++++++++++ src/npc.cpp | 26 +- src/npc.h | 136 ++++++++-- src/npcmove.cpp | 5 + src/npctalk.cpp | 358 +++++++++++++++++++-------- src/savegame_json.cpp | 43 +++- src/ui.cpp | 12 +- src/ui.h | 1 + tests/npc_talk_test.cpp | 2 +- 10 files changed, 647 insertions(+), 167 deletions(-) diff --git a/data/json/npcs/TALK_COMMON_ALLY.json b/data/json/npcs/TALK_COMMON_ALLY.json index c46d0bf214f12..e246f81c0e28d 100644 --- a/data/json/npcs/TALK_COMMON_ALLY.json +++ b/data/json/npcs/TALK_COMMON_ALLY.json @@ -154,11 +154,11 @@ }, { "and": [ - { "npc_override": "avoid_combat", "yes": " OVERRIDE: ", "no": " " }, + { "npc_override": "follow_close", "yes": " OVERRIDE: ", "no": " " }, { - "npc_rule": "avoid_combat", - "yes": " will follow you instead of fighting.", - "no": " will fight instead of following you." + "npc_rule": "follow_close", + "yes": "", + "no": "" } ] }, @@ -172,12 +172,12 @@ { "npc_override": "use_silent", "yes": " OVERRIDE: ", "no": " " }, { "npc_rule": "use_silent", - "yes": " will use silenced ranged weapons.", - "no": " will use ranged weapons." + "yes": "", + "no": "" } ] }, - "no": " will not use ranged weapons." + "no": "" } ] }, @@ -186,8 +186,8 @@ { "npc_override": "use_grenades", "yes": " OVERRIDE: ", "no": " " }, { "npc_rule": "use_grenades", - "yes": " will use grenades.", - "no": " will not use grenades." + "yes": "", + "no": "" } ] }, @@ -196,8 +196,8 @@ { "npc_override": "avoid_friendly_fire", "yes": " OVERRIDE: ", "no": " " }, { "npc_rule": "avoid_friendly_fire", - "yes": " will avoid shooting if allies are in the line of fire.", - "no": " will shoot even if allies are in the line of fire." + "yes": "", + "no": "" } ] }, @@ -206,8 +206,8 @@ { "npc_override": "hold_the_line", "yes": " OVERRIDE: ", "no": " " }, { "npc_rule": "hold_the_line", - "yes": " will hold the line by not moving into doorways or obstructions adjacent to you.", - "no": " will move freely to attack enemies." + "yes": "", + "no": "" } ] }, @@ -219,12 +219,12 @@ { "text": "Change your aiming rules...", "topic": "TALK_AIM_RULES" }, { "truefalsetext": { - "condition": { "npc_rule": "avoid_combat" }, - "true": "Stand and fight.", - "false": "If you see me running away, you follow me." + "condition": { "npc_rule": "follow_close" }, + "true": "Move freely as you need to.", + "false": "Stick close to me, no matter what." }, "topic": "TALK_COMBAT_COMMANDS", - "effect": { "toggle_npc_rule": "avoid_combat" } + "effect": { "toggle_npc_rule": "follow_close" } }, { "truefalsetext": { @@ -405,8 +405,8 @@ { "npc_override": "allow_pick_up", "yes": "OVERRIDE: " }, { "npc_rule": "allow_pick_up", - "yes": { "has_pickup_list": "* will pick up items from the whitelist.", "no": "* will pick up all items." }, - "no": "* will not pick up items." + "yes": { "has_pickup_list": "", "no": "" }, + "no": "" } ] }, @@ -415,8 +415,8 @@ { "npc_override": "allow_bash", "yes": " OVERRIDE: ", "no": " " }, { "npc_rule": "allow_bash", - "yes": " will bash down obstacles.", - "no": " will not bash down obstacles." + "yes": "", + "no": "" } ] }, @@ -425,8 +425,8 @@ { "npc_override": "allow_sleep", "yes": " OVERRIDE: ", "no": " " }, { "npc_rule": "allow_sleep", - "yes": " will sleep when tired.", - "no": " will sleep only when exhausted." + "yes": "", + "no": "" } ] }, @@ -435,8 +435,8 @@ { "npc_override": "allow_complain", "yes": " OVERRIDE: ", "no": " " }, { "npc_rule": "allow_complain", - "yes": " will complain about wounds and needs.", - "no": " will only complain in an emergency." + "yes": "", + "no": "" } ] }, @@ -445,8 +445,8 @@ { "npc_override": "allow_pulp", "yes": " OVERRIDE: ", "no": " " }, { "npc_rule": "allow_pulp", - "yes": " will smash nearby zombie corpses.", - "no": " will leave zombie corpses intact." + "yes": "", + "no": "" } ] }, @@ -455,8 +455,8 @@ { "npc_override": "close_doors", "yes": " OVERRIDE: ", "no": " " }, { "npc_rule": "close_doors", - "yes": " will close doors behind themselves.", - "no": " will leave doors open." + "yes": "", + "no": "" } ] }, @@ -465,8 +465,8 @@ { "npc_override": "ignore_noise", "yes": " OVERRIDE: ", "no": " " }, { "npc_rule": "ignore_noise", - "yes": " will not investigate noises.", - "no": " will investigate noises coming from unseen places." + "yes": "", + "no": "" } ] }, @@ -475,8 +475,18 @@ { "npc_override": "avoid_doors", "yes": " OVERRIDE: ", "no": " " }, { "npc_rule": "avoid_doors", - "yes": " will not go places that require opening a door.", - "no": " will open doors to reach a destination." + "yes": "", + "no": "" + } + ] + }, + { + "and": [ + { "npc_override": "forbid_engage", "yes": " OVERRIDE: ", "no": " " }, + { + "npc_rule": "forbid_engage", + "yes": "", + "no": "" } ] } diff --git a/data/json/npcs/talk_tags.json b/data/json/npcs/talk_tags.json index 505f602790bb7..a6a05bcb16482 100644 --- a/data/json/npcs/talk_tags.json +++ b/data/json/npcs/talk_tags.json @@ -994,5 +994,160 @@ "type": "snippet", "category": "", "text": [ "child", "my child", "dear", "my dear", "friend", "survivor" ] + }, + { + "type": "snippet", + "category": "", + "text": " will use ranged weapons." + }, + { + "type": "snippet", + "category": "", + "text": " will not use ranged weapons." + }, + { + "type": "snippet", + "category": "", + "text": " will use grenades." + }, + { + "type": "snippet", + "category": "", + "text": " will not use grenades." + }, + { + "type": "snippet", + "category": "", + "text": " will only use silenced ranged weapons." + }, + { + "type": "snippet", + "category": "", + "text": " will use any ranged weapons." + }, + { + "type": "snippet", + "category": "", + "text": " will avoid shooting if allies are in the line of fire." + }, + { + "type": "snippet", + "category": "", + "text": " will shoot even if allies are in the line of fire." + }, + { + "type": "snippet", + "category": "", + "text": "* will pick up items." + }, + { + "type": "snippet", + "category": "", + "text": "* will only pick up items from the whitelist." + }, + { + "type": "snippet", + "category": "", + "text": "* will not pick up items." + }, + { + "type": "snippet", + "category": "", + "text": " will bash down obstacles." + }, + { + "type": "snippet", + "category": "", + "text": " will not bash down obstacles." + }, + { + "type": "snippet", + "category": "", + "text": " will sleep when tired." + }, + { + "type": "snippet", + "category": "", + "text": " will stay awake as long as possible." + }, + { + "type": "snippet", + "category": "", + "text": " will complain about wounds and needs." + }, + { + "type": "snippet", + "category": "", + "text": " will only complain in an emergency." + }, + { + "type": "snippet", + "category": "", + "text": " will smash nearby zombie corpses." + }, + { + "type": "snippet", + "category": "", + "text": " will leave zombie corpses intact." + }, + { + "type": "snippet", + "category": "", + "text": " will close doors after passing through." + }, + { + "type": "snippet", + "category": "", + "text": " will not close doors." + }, + { + "type": "snippet", + "category": "", + "text": " will follow you closely even when threatened." + }, + { + "type": "snippet", + "category": "", + "text": " will move freely as needed." + }, + { + "type": "snippet", + "category": "", + "text": " will not go places that require opening a door." + }, + { + "type": "snippet", + "category": "", + "text": " will open doors to reach a destination." + }, + { + "type": "snippet", + "category": "", + "text": " will hold the line by not moving into doorways or obstructions adjacent to you." + }, + { + "type": "snippet", + "category": "", + "text": " will move freely to attack enemies." + }, + { + "type": "snippet", + "category": "", + "text": " will not investigate noises." + }, + { + "type": "snippet", + "category": "", + "text": " will investigate noises from unseen places." + }, + { + "type": "snippet", + "category": "", + "text": " will not engage enemies if avoidable." + }, + { + "type": "snippet", + "category": "", + "text": " will follow normal engagement rules." } ] diff --git a/src/npc.cpp b/src/npc.cpp index 6c5ab1f146fc3..205570afcc52f 100644 --- a/src/npc.cpp +++ b/src/npc.cpp @@ -2600,10 +2600,11 @@ npc_follower_rules::npc_follower_rules() set_flag( ally_rule::allow_complain ); set_flag( ally_rule::allow_pulp ); clear_flag( ally_rule::close_doors ); - clear_flag( ally_rule::avoid_combat ); + clear_flag( ally_rule::follow_close ); clear_flag( ally_rule::avoid_doors ); clear_flag( ally_rule::hold_the_line ); clear_flag( ally_rule::ignore_noise ); + clear_flag( ally_rule::forbid_engage ); } bool npc_follower_rules::has_flag( ally_rule test, bool check_override ) const @@ -2639,6 +2640,25 @@ void npc_follower_rules::toggle_flag( ally_rule toggle ) } } +void npc_follower_rules::set_specific_override_state( ally_rule rule, bool state ) +{ + if( state ) { + set_override( rule ); + } else { + clear_override( rule ); + } + enable_override( rule ); +} + +void npc_follower_rules::toggle_specific_override_state( ally_rule rule, bool state ) +{ + if( has_override_enable( rule ) && has_override( rule ) == state ) { + clear_override( rule ); + disable_override( rule ); + } else { + set_specific_override_state( rule, state ); + } +} bool npc::is_hallucination() const { @@ -2682,10 +2702,10 @@ void npc_follower_rules::set_danger_overrides() { overrides = ally_rule::DEFAULT; override_enable = ally_rule::DEFAULT; - set_override( ally_rule::avoid_combat ); + set_override( ally_rule::follow_close ); set_override( ally_rule::avoid_doors ); set_override( ally_rule::hold_the_line ); - enable_override( ally_rule::avoid_combat ); + enable_override( ally_rule::follow_close ); enable_override( ally_rule::allow_sleep ); enable_override( ally_rule::close_doors ); enable_override( ally_rule::avoid_doors ); diff --git a/src/npc.h b/src/npc.h index c6d15c94eec85..a3757c43120c5 100644 --- a/src/npc.h +++ b/src/npc.h @@ -25,6 +25,7 @@ #include "enums.h" #include "inventory.h" #include "item_location.h" +#include "translations.h" #include "string_formatter.h" #include "string_id.h" #include "material.h" @@ -237,26 +238,125 @@ enum class ally_rule { allow_complain = 128, allow_pulp = 256, close_doors = 512, - avoid_combat = 1024, + follow_close = 1024, avoid_doors = 2048, hold_the_line = 4096, - ignore_noise = 8192 + ignore_noise = 8192, + forbid_engage = 16384 }; -const std::unordered_map ally_rule_strs = { { - { "use_guns", ally_rule::use_guns }, - { "use_grenades", ally_rule::use_grenades }, - { "use_silent", ally_rule::use_silent }, - { "avoid_friendly_fire", ally_rule::avoid_friendly_fire }, - { "allow_pick_up", ally_rule::allow_pick_up }, - { "allow_bash", ally_rule::allow_bash }, - { "allow_sleep", ally_rule::allow_sleep }, - { "allow_complain", ally_rule::allow_complain }, - { "allow_pulp", ally_rule::allow_pulp }, - { "close_doors", ally_rule::close_doors }, - { "avoid_combat", ally_rule::avoid_combat }, - { "avoid_doors", ally_rule::avoid_doors }, - { "hold_the_line", ally_rule::hold_the_line }, - { "ignore_noise", ally_rule::ignore_noise } + +struct ally_rule_data { + ally_rule rule; + std::string rule_true_text; + std::string rule_false_text; +}; + +const std::unordered_map ally_rule_strs = { { + { + "use_guns", { + ally_rule::use_guns, + "", + "" + } + }, + { + "use_grenades", { + ally_rule::use_grenades, + "", + "" + } + }, + { + "use_silent", { + ally_rule::use_silent, + "", + "" + } + }, + { + "avoid_friendly_fire", { + ally_rule::avoid_friendly_fire, + "", + "" + } + }, + { + "allow_pick_up", { + ally_rule::allow_pick_up, + "", + "" + } + }, + { + "allow_bash", { + ally_rule::allow_bash, + "", + "" + } + }, + { + "allow_sleep", { + ally_rule::allow_sleep, + "", + "" + } + }, + { + "allow_complain", { + ally_rule::allow_complain, + "", + "" + } + }, + { + "allow_pulp", { + ally_rule::allow_pulp, + "", + "" + } + }, + { + "close_doors", { + ally_rule::close_doors, + "", + "" + } + }, + { + "follow_close", { + ally_rule::follow_close, + "", + "" + } + }, + { + "avoid_doors", { + ally_rule::avoid_doors, + "", + "" + } + }, + { + "hold_the_line", { + ally_rule::hold_the_line, + "", + "" + } + }, + { + "ignore_noise", { + ally_rule::ignore_noise, + "", + "" + } + }, + { + "forbid_engage", { + ally_rule::forbid_engage, + "", + "" + } + } } }; @@ -278,6 +378,8 @@ struct npc_follower_rules { void set_flag( ally_rule setit ); void clear_flag( ally_rule clearit ); void toggle_flag( ally_rule toggle ); + void set_specific_override_state( ally_rule, bool state ); + void toggle_specific_override_state( ally_rule rule, bool state ); bool has_override_enable( ally_rule test ) const; void enable_override( ally_rule setit ); void disable_override( ally_rule setit ); diff --git a/src/npcmove.cpp b/src/npcmove.cpp index 632b0ac377310..a5f7f969e8273 100644 --- a/src/npcmove.cpp +++ b/src/npcmove.cpp @@ -289,12 +289,17 @@ void npc::assess_danger() float assessment = 0.0f; float highest_priority = 1.0f; + // Radius we can attack without moving const int max_range = std::max( weapon.reach_range( *this ), confident_shoot_range( weapon, get_most_accurate_sight( weapon ) ) ); const auto ok_by_rules = [max_range, this]( const Creature & c, int dist, int scaled_dist ) { + // If we're forbidden to attack, no need to check engagement rules + if( rules.has_flag( ally_rule::forbid_engage ) ) { + return false; + } switch( rules.engagement ) { case ENGAGE_NONE: return false; diff --git a/src/npctalk.cpp b/src/npctalk.cpp index 078211602911b..b761781580bfa 100644 --- a/src/npctalk.cpp +++ b/src/npctalk.cpp @@ -163,6 +163,149 @@ int cash_to_favor( const npc &, int cash ) return roll_remainder( scaled_mission_val ); } +enum npc_chat_menu { + NPC_CHAT_DONE, + NPC_CHAT_TALK, + NPC_CHAT_YELL, + NPC_CHAT_SENTENCE, + NPC_CHAT_GUARD, + NPC_CHAT_FOLLOW, + NPC_CHAT_AWAKE, + NPC_CHAT_DANGER, + NPC_CHAT_ORDERS, + NPC_CHAT_NO_GUNS, + NPC_CHAT_PULP, + NPC_CHAT_FOLLOW_CLOSE, + NPC_CHAT_MOVE_FREELY, + NPC_CHAT_SLEEP, + NPC_CHAT_FORBID_ENGAGE, + NPC_CHAT_CLEAR_OVERRIDES +}; + +// given a vector of NPCs, presents a menu to allow a player to pick one. +// everyone == true adds another entry at the end to allow selecting all listed NPCs +// this implies a return value of npc_list.size() means "everyone" +int npc_select_menu( const std::vector &npc_list, const std::string prompt, + const bool everyone = true ) +{ + if( npc_list.empty() ) { + return -1; + } + const int npc_count = npc_list.size(); + if( npc_count == 1 ) { + return 0; + } else { + uilist nmenu; + nmenu.text = prompt; + for( auto &elem : npc_list ) { + nmenu.addentry( -1, true, MENU_AUTOASSIGN, ( elem )->name ); + } + if( npc_count > 1 && everyone ) { + nmenu.addentry( -1, true, MENU_AUTOASSIGN, _( "Everyone" ) ); + } + nmenu.query(); + return nmenu.ret; + } + +} + +void npc_batch_override_toggle( const std::vector npc_list, ally_rule rule, bool state ) +{ + for( npc *p : npc_list ) { + p->rules.toggle_specific_override_state( rule, state ); + } +} + +void npc_temp_orders_menu( const std::vector &npc_list ) +{ + if( npc_list.empty() ) { + return; + } + npc *guy = npc_list.front(); + + bool done = false; + uilist nmenu; + + while( !done ) { + int override_count = 0; + std::ostringstream override_string; + override_string << string_format( _( "%s currently has these temporary orders:" ), guy->name ); + for( const auto &rule : ally_rule_strs ) { + if( guy->rules.has_override_enable( rule.second.rule ) ) { + override_count++; + override_string << "\n "; + override_string << ( guy->rules.has_override( rule.second.rule ) ? + rule.second.rule_true_text : rule.second.rule_false_text ); + } + } + if( override_count == 0 ) { + override_string << "\n " << _( "None." ) << "\n"; + } + if( npc_list.size() > 1 ) { + override_string << "\n" << _( "Other followers might have different temporary orders." ); + } + g->refresh_all(); + nmenu.reset(); + nmenu.text = _( "Issue what temporary order?" ); + nmenu.desc_enabled = true; + std::string output_string = override_string.str(); + parse_tags( output_string, g->u, *guy ); + nmenu.footer_text = output_string; + nmenu.addentry( NPC_CHAT_DONE, true, 'd', _( "Done issuing orders" ) ); + nmenu.addentry( NPC_CHAT_FORBID_ENGAGE, true, 'f', + guy->rules.has_override_enable( ally_rule::forbid_engage ) ? + _( "Go back to your usual engagement habits" ) : _( "Don't engage hostiles for the time being" ) ); + nmenu.addentry( NPC_CHAT_NO_GUNS, true, 'g', guy->rules.has_override_enable( ally_rule::use_guns ) ? + _( "Use whatever weapon you normally would" ) : _( "Don't use ranged weapons for a while" ) ); + nmenu.addentry( NPC_CHAT_PULP, true, 'p', guy->rules.has_override_enable( ally_rule::allow_pulp ) ? + _( "Pulp zombies if you like" ) : _( "Hold off on pulping zombies for a while" ) ); + nmenu.addentry( NPC_CHAT_FOLLOW_CLOSE, true, 'c', + guy->rules.has_override_enable( ally_rule::follow_close ) && + guy->rules.has_override( ally_rule::follow_close ) ? + _( "Go back to keeping your usual distance" ) : _( "Stick close to me for now" ) ); + nmenu.addentry( NPC_CHAT_MOVE_FREELY, true, 'm', + guy->rules.has_override_enable( ally_rule::follow_close ) && + !guy->rules.has_override( ally_rule::follow_close ) ? + _( "Go back to keeping your usual distance" ) : _( "Move farther from me if you need to" ) ); + nmenu.addentry( NPC_CHAT_SLEEP, true, 's', + guy->rules.has_override_enable( ally_rule::allow_sleep ) ? + _( "Go back to your usual sleeping habits" ) : _( "Take a nap if you need it" ) ); + nmenu.addentry( NPC_CHAT_CLEAR_OVERRIDES, true, 'o', _( "Let's go back to your usual behaviors" ) ); + nmenu.query(); + + switch( nmenu.ret ) { + case NPC_CHAT_FORBID_ENGAGE: + npc_batch_override_toggle( npc_list, ally_rule::forbid_engage, true ); + break; + case NPC_CHAT_NO_GUNS: + npc_batch_override_toggle( npc_list, ally_rule::use_guns, false ); + break; + case NPC_CHAT_PULP: + npc_batch_override_toggle( npc_list, ally_rule::allow_pulp, false ); + break; + case NPC_CHAT_FOLLOW_CLOSE: + npc_batch_override_toggle( npc_list, ally_rule::follow_close, true ); + break; + case NPC_CHAT_MOVE_FREELY: + npc_batch_override_toggle( npc_list, ally_rule::follow_close, false ); + break; + case NPC_CHAT_SLEEP: + npc_batch_override_toggle( npc_list, ally_rule::allow_sleep, true ); + break; + case NPC_CHAT_CLEAR_OVERRIDES: + for( npc *p : npc_list ) { + p->rules.clear_overrides(); + } + break; + default: + done = true; + break; + } + } + +} + + void game::chat() { int volume = g->u.get_shout_volume(); @@ -172,119 +315,140 @@ void game::chat() return u.posz() == guy.posz() && u.sees( guy.pos() ) && rl_dist( u.pos(), guy.pos() ) <= SEEX * 2; } ); + const int available_count = available.size(); const std::vector followers = get_npcs_if( [&]( const npc & guy ) { return guy.is_player_ally() && guy.is_following() && guy.can_hear( u.pos(), volume ); } ); + const int follower_count = followers.size(); const std::vector guards = get_npcs_if( [&]( const npc & guy ) { return guy.mission == NPC_MISSION_GUARD_ALLY && guy.companion_mission_role_id != "FACTION_CAMP" && guy.can_hear( u.pos(), volume ); } ); + const int guard_count = guards.size(); uilist nmenu; - nmenu.text = std::string( _( "Who do you want to talk to or yell at?" ) ); + nmenu.text = std::string( _( "What do you want to do?" ) ); - int i = 0; - - for( auto &elem : available ) { - nmenu.addentry( i++, true, MENU_AUTOASSIGN, ( elem )->name ); - } - - int yell = 0; - int yell_sentence = 0; - int yell_guard = -1; - int yell_follow = -1; - int yell_awake = -1; - int yell_sleep = -1; - int yell_flee = -1; - int yell_stop = -1; - int yell_danger = -1; - int yell_relax = -1; - - nmenu.addentry( yell = i++, true, 'a', _( "Yell" ) ); - nmenu.addentry( yell_sentence = i++, true, 'b', _( "Yell a sentence" ) ); - if( !followers.empty() ) { - nmenu.addentry( yell_guard = i++, true, 'g', _( "Tell all your allies to guard" ) ); - nmenu.addentry( yell_awake = i++, true, 'w', _( "Tell all your allies to stay awake" ) ); - nmenu.addentry( yell_sleep = i++, true, 's', - _( "Tell all your allies to relax and sleep when tired" ) ); - nmenu.addentry( yell_flee = i++, true, 'R', _( "Tell all your allies to flee" ) ); - nmenu.addentry( yell_stop = i++, true, 'S', _( "Tell all your allies stop running" ) ); - nmenu.addentry( yell_danger = i++, true, 'D', - _( "Tell all your allies to prepare for danger" ) ); - nmenu.addentry( yell_relax = i++, true, 'C', - _( "Tell all your allies to relax from danger" ) ); + if( !available.empty() ) { + nmenu.addentry( NPC_CHAT_TALK, true, 't', available_count == 1 ? + string_format( _( "Talk to %s" ), available.front()->name ) : + _( "Talk to ..." ) + ); } + nmenu.addentry( NPC_CHAT_YELL, true, 'a', _( "Yell" ) ); + nmenu.addentry( NPC_CHAT_SENTENCE, true, 'b', _( "Yell a sentence" ) ); if( !guards.empty() ) { - nmenu.addentry( yell_follow = i++, true, 'f', _( "Tell all your allies to follow" ) ); + nmenu.addentry( NPC_CHAT_FOLLOW, true, 'f', guard_count == 1 ? + string_format( _( "Tell %s to follow" ), guards.front()->name ) : + _( "Tell someone to follow..." ) + ); + } + if( !followers.empty() ) { + nmenu.addentry( NPC_CHAT_GUARD, true, 'g', follower_count == 1 ? + string_format( _( "Tell %s to guard" ), followers.front()->name ) : + _( "Tell someone to guard..." ) + ); + nmenu.addentry( NPC_CHAT_AWAKE, true, 'w', _( "Tell everyone on your team to wake up" ) ); + nmenu.addentry( NPC_CHAT_DANGER, true, 'D', + _( "Tell everyone on your team to prepare for danger" ) ); + nmenu.addentry( NPC_CHAT_CLEAR_OVERRIDES, true, 'r', + _( "Tell everyone on your team to relax (Clear Overrides)" ) ); + nmenu.addentry( NPC_CHAT_ORDERS, true, 'o', _( "Tell everyone on your team to temporarily..." ) ); } - std::string message; std::string yell_msg; bool is_order = true; nmenu.query(); + if( nmenu.ret < 0 ) { return; - } else if( nmenu.ret == yell ) { - is_order = false; - message = _( "loudly." ); - } else if( nmenu.ret == yell_sentence ) { - std::string popupdesc = string_format( _( "Enter a sentence to yell" ) ); - string_input_popup popup; - popup.title( string_format( _( "Yell a sentence" ) ) ) - .width( 64 ) - .description( popupdesc ) - .identifier( "sentence" ) - .max_length( 128 ) - .query(); - yell_msg = popup.text() + "."; - is_order = false; - } else if( nmenu.ret == yell_guard ) { - for( npc *p : followers ) { - talk_function::assign_guard( *p ); - } - yell_msg = _( "Guard here!" ); - } else if( nmenu.ret == yell_awake ) { - for( npc *p : followers ) { - talk_function::wake_up( *p ); - } - yell_msg = _( "Stay awake!" ); - } else if( nmenu.ret == yell_sleep ) { - for( npc *p : followers ) { - p->rules.set_flag( ally_rule::allow_sleep ); - } - yell_msg = _( "We're safe! Take a nap if you're tired." ); - } else if( nmenu.ret == yell_follow ) { - for( npc *p : guards ) { - talk_function::stop_guard( *p ); - } - yell_msg = _( "Follow me!" ); - } else if( nmenu.ret == yell_flee ) { - for( npc *p : followers ) { - p->rules.set_flag( ally_rule::avoid_combat ); - } - yell_msg = _( "Fall back to safety! Flee, you fools!" ); - } else if( nmenu.ret == yell_stop ) { - for( npc *p : followers ) { - p->rules.clear_flag( ally_rule::avoid_combat ); - } - yell_msg = _( "No need to run any more, we can fight here." ); - } else if( nmenu.ret == yell_danger ) { - for( npc *p : followers ) { - p->rules.set_danger_overrides(); - } - yell_msg = _( "We're in danger. Stay awake, stay close, don't go wandering off, " - "and don't open any doors." ); - } else if( nmenu.ret == yell_relax ) { - for( npc *p : followers ) { - talk_function::clear_overrides( *p ); - } - yell_msg = _( "Relax and stand down." ); - } else if( nmenu.ret <= static_cast( available.size() ) ) { - available[nmenu.ret]->talk_to_u(); - } else { - return; } + + switch( nmenu.ret ) { + case NPC_CHAT_TALK: { + const int npcselect = npc_select_menu( available, _( "Talk to whom?" ), false ); + if( npcselect < 0 ) { + return; + } + available[npcselect]->talk_to_u(); + break; + } + case NPC_CHAT_YELL: + is_order = false; + message = _( "loudly." ); + break; + case NPC_CHAT_SENTENCE: { + std::string popupdesc = string_format( _( "Enter a sentence to yell" ) ); + string_input_popup popup; + popup.title( string_format( _( "Yell a sentence" ) ) ) + .width( 64 ) + .description( popupdesc ) + .identifier( "sentence" ) + .max_length( 128 ) + .query(); + yell_msg = popup.text() + "."; + is_order = false; + break; + } + case NPC_CHAT_GUARD: { + const int npcselect = npc_select_menu( followers, _( "Who should guard here?" ) ); + if( npcselect < 0 ) { + return; + } + if( npcselect == follower_count ) { + for( npc *them : followers ) { + talk_function::assign_guard( *them ); + } + yell_msg = _( "Everyone guard here!" ); + } else { + talk_function::assign_guard( *followers[npcselect] ); + yell_msg = string_format( _( "Guard here, %s!" ), followers[npcselect]->name ); + } + break; + } + case NPC_CHAT_FOLLOW: { + const int npcselect = npc_select_menu( guards, _( "Who should follow you?" ) ); + if( npcselect < 0 ) { + return; + } + if( npcselect == guard_count ) { + for( npc *them : guards ) { + talk_function::assign_guard( *them ); + } + yell_msg = _( "Everyone follow me!" ); + } else { + talk_function::stop_guard( *guards[npcselect] ); + yell_msg = string_format( _( "Follow me, %s!" ), guards[npcselect]->name ); + } + break; + } + case NPC_CHAT_AWAKE: + for( npc *them : followers ) { + talk_function::wake_up( *them ); + } + yell_msg = _( "Stay awake!" ); + break; + case NPC_CHAT_DANGER: + for( npc *them : followers ) { + them->rules.set_danger_overrides(); + } + yell_msg = _( "We're in danger. Stay awake, stay close, don't go wandering off, " + "and don't open any doors." ); + break; + case NPC_CHAT_CLEAR_OVERRIDES: + for( npc *p : followers ) { + talk_function::clear_overrides( *p ); + } + yell_msg = _( "As you were." ); + break; + case NPC_CHAT_ORDERS: + npc_temp_orders_menu( followers ); + break; + default: + return; + } + if( !yell_msg.empty() ) { message = string_format( "\"%s\"", yell_msg ); } @@ -1726,7 +1890,7 @@ void talk_effect_fun_t::set_toggle_npc_rule( const std::string &rule ) if( toggle == ally_rule_strs.end() ) { return; } - d.beta->rules.toggle_flag( toggle->second ); + d.beta->rules.toggle_flag( toggle->second.rule ); d.beta->wield_better_weapon(); }; } @@ -1738,7 +1902,7 @@ void talk_effect_fun_t::set_set_npc_rule( const std::string &rule ) if( flag == ally_rule_strs.end() ) { return; } - d.beta->rules.set_flag( flag->second ); + d.beta->rules.set_flag( flag->second.rule ); d.beta->wield_better_weapon(); }; } @@ -1750,7 +1914,7 @@ void talk_effect_fun_t::set_clear_npc_rule( const std::string &rule ) if( flag == ally_rule_strs.end() ) { return; } - d.beta->rules.clear_flag( flag->second ); + d.beta->rules.clear_flag( flag->second.rule ); d.beta->wield_better_weapon(); }; } @@ -2599,7 +2763,7 @@ void conditional_t::set_npc_rule( JsonObject &jo ) condition = [rule]( const dialogue & d ) { auto flag = ally_rule_strs.find( rule ); if( flag != ally_rule_strs.end() ) { - return d.beta->rules.has_flag( flag->second ); + return d.beta->rules.has_flag( flag->second.rule ); } return false; }; @@ -2611,7 +2775,7 @@ void conditional_t::set_npc_override( JsonObject &jo ) condition = [rule]( const dialogue & d ) { auto flag = ally_rule_strs.find( rule ); if( flag != ally_rule_strs.end() ) { - return d.beta->rules.has_override_enable( flag->second ); + return d.beta->rules.has_override_enable( flag->second.rule ); } return false; }; diff --git a/src/savegame_json.cpp b/src/savegame_json.cpp index 2bbe840267a69..09562cc7e3ab4 100644 --- a/src/savegame_json.cpp +++ b/src/savegame_json.cpp @@ -1053,13 +1053,13 @@ void npc_follower_rules::serialize( JsonOut &json ) const // serialize the flags so they can be changed between save games for( const auto &rule : ally_rule_strs ) { - json.member( "rule_" + rule.first, has_flag( rule.second, false ) ); + json.member( "rule_" + rule.first, has_flag( rule.second.rule, false ) ); } for( const auto &rule : ally_rule_strs ) { - json.member( "override_enable_" + rule.first, has_override_enable( rule.second ) ); + json.member( "override_enable_" + rule.first, has_override_enable( rule.second.rule ) ); } for( const auto &rule : ally_rule_strs ) { - json.member( "override_" + rule.first, has_override( rule.second ) ); + json.member( "override_" + rule.first, has_override( rule.second.rule ) ); } json.member( "pickup_whitelist", *pickup_whitelist ); @@ -1083,28 +1083,49 @@ void npc_follower_rules::deserialize( JsonIn &jsin ) // legacy to handle rules that were saved before overrides data.read( rule.first, tmpflag ); if( tmpflag ) { - set_flag( rule.second ); + set_flag( rule.second.rule ); } else { - clear_flag( rule.second ); + clear_flag( rule.second.rule ); } data.read( "rule_" + rule.first, tmpflag ); if( tmpflag ) { - set_flag( rule.second ); + set_flag( rule.second.rule ); } else { - clear_flag( rule.second ); + clear_flag( rule.second.rule ); } data.read( "override_enable_" + rule.first, tmpflag ); if( tmpflag ) { - enable_override( rule.second ); + enable_override( rule.second.rule ); } else { - disable_override( rule.second ); + disable_override( rule.second.rule ); } data.read( "override_" + rule.first, tmpflag ); if( tmpflag ) { - set_override( rule.second ); + set_override( rule.second.rule ); } else { - clear_override( rule.second ); + clear_override( rule.second.rule ); } + + // This and the following two entries are for legacy save game handling. + // "avoid_combat" was renamed "follow_close" to better reflect behavior. + data.read( "rule_avoid_combat", tmpflag ); + if( tmpflag ) { + set_flag( ally_rule::follow_close ); + } else { + clear_flag( ally_rule::follow_close ); + }; + data.read( "override_enable_avoid_combat", tmpflag ); + if( tmpflag ) { + enable_override( ally_rule::follow_close ); + } else { + disable_override( ally_rule::follow_close ); + }; + data.read( "override_avoid_combat", tmpflag ); + if( tmpflag ) { + set_override( ally_rule::follow_close ); + } else { + clear_override( ally_rule::follow_close ); + }; } data.read( "pickup_whitelist", *pickup_whitelist ); diff --git a/src/ui.cpp b/src/ui.cpp index c39d6412bb4a8..9d42dec37caee 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -143,7 +143,8 @@ void uilist::init() pad_right = 0; // or right desc_enabled = false; // don't show option description by default desc_lines = 6; // default number of lines for description - border = true; // TODO: always true + footer_text.clear(); // takes precedence over per-entry descriptions. + border = true; // TODO: always true. border_color = c_magenta; // border color text_color = c_light_gray; // text color title_color = c_green; // title color @@ -391,8 +392,8 @@ void uilist::setup() if( desc_enabled ) { const int min_width = std::min( TERMX, std::max( w_width, descwidth_final ) ) - 4; const int max_width = TERMX - 4; - int descwidth = find_minimum_fold_width( entries[i].desc, desc_lines, - min_width, max_width ); + int descwidth = find_minimum_fold_width( footer_text.empty() ? entries[i].desc : footer_text, + desc_lines, min_width, max_width ); descwidth += 4; // 2x border + 2x ' ' pad if( descwidth_final < descwidth ) { descwidth_final = descwidth; @@ -470,7 +471,8 @@ void uilist::setup() desc_lines = 0; for( const uilist_entry &ent : entries ) { // -2 for borders, -2 for padding - desc_lines = std::max( desc_lines, foldstring( ent.desc, w_width - 4 ).size() ); + desc_lines = std::max( desc_lines, foldstring( footer_text.empty() ? ent.desc : footer_text, + w_width - 4 ).size() ); } if( desc_lines <= 0 ) { desc_enabled = false; @@ -679,7 +681,7 @@ void uilist::show() if( static_cast( selected ) < entries.size() ) { fold_and_print( window, w_height - desc_lines - 1, 2, w_width - 4, text_color, - entries[selected].desc ); + footer_text.empty() ? entries[selected].desc : footer_text ); } } diff --git a/src/ui.h b/src/ui.h index 7bcfb5f520399..e75b7ec3c3a06 100644 --- a/src/ui.h +++ b/src/ui.h @@ -174,6 +174,7 @@ class uilist: public ui_container std::map keymap; bool desc_enabled; int desc_lines; + std::string footer_text; // basically the same as desc, except it doesn't change based on selection bool border; bool filtering; bool filtering_nocase; diff --git a/tests/npc_talk_test.cpp b/tests/npc_talk_test.cpp index 98ab951728efb..5f46ebb15522d 100644 --- a/tests/npc_talk_test.cpp +++ b/tests/npc_talk_test.cpp @@ -559,7 +559,7 @@ TEST_CASE( "npc_talk_test" ) gen_response_lines( d, 9 ); CHECK( d.responses[0].text == "Change your engagement rules..." ); CHECK( d.responses[1].text == "Change your aiming rules..." ); - CHECK( d.responses[2].text == "If you see me running away, you follow me." ); + CHECK( d.responses[2].text == "Stick close to me, no matter what." ); CHECK( d.responses[3].text == "Don't use ranged weapons anymore." ); CHECK( d.responses[4].text == "Use only silent weapons." ); CHECK( d.responses[5].text == "Don't use grenades anymore." );