From ce1b434a1aac2086c251b9844a55416f48432728 Mon Sep 17 00:00:00 2001 From: Mark Langsdorf Date: Fri, 25 Jan 2019 05:37:37 -0600 Subject: [PATCH] npctalk: NPC AI follower rules to JSON Change npc_follower_rules to use a enum bitfield instead of individual booleans to make it easier to write a single function to toggle the rules. Then create functions to toggle NPC follower AI rules and move all that dialogue into JSON. --- data/json/npcs/TALK_COMMON_ALLY.json | 414 +++++++++++++++++++++++++ data/json/npcs/TALK_COMMON_OTHER.json | 70 +---- data/json/npcs/TALK_TEST.json | 19 ++ doc/NPCs.md | 38 +++ src/dialogue.h | 4 + src/npc.cpp | 43 ++- src/npc.h | 60 +++- src/npcmove.cpp | 23 +- src/npctalk.cpp | 428 +++++++------------------- src/npctalk_funcs.cpp | 2 +- src/savegame_json.cpp | 35 +-- tests/npc_talk_test.cpp | 23 ++ 12 files changed, 724 insertions(+), 435 deletions(-) create mode 100644 data/json/npcs/TALK_COMMON_ALLY.json diff --git a/data/json/npcs/TALK_COMMON_ALLY.json b/data/json/npcs/TALK_COMMON_ALLY.json new file mode 100644 index 0000000000000..c7626dd90161d --- /dev/null +++ b/data/json/npcs/TALK_COMMON_ALLY.json @@ -0,0 +1,414 @@ +[ + { + "id": "TALK_WAKE_UP", + "type": "talk_topic", + "dynamic_line": { + "npc_has_effect": "sleep", + "yes": { + "npc_need": "fatigue", + "level": "EXHAUSTED", + "no": { + "npc_need": "fatigue", + "level": "DEAD_TIRED", + "no": { + "npc_need": "fatigue", + "level": "TIRED", + "no": "Just few minutes more...", + "yes": "Make it quick, I want to go back to sleep." + }, + "yes": "Just let me sleep, !" + }, + "yes": "No, just no..." + }, + "no": "Anything to do before I go to sleep?" + }, + "responses": [ + { "text": "Wake up!", "topic": "TALK_NONE", "effect": "wake_up" }, + { "text": "Go back to sleep.", "topic": "TALK_DONE" } + ] + }, + { + "id": [ "TALK_FRIEND", "TALK_GIVE_ITEM", "TALK_USE_ITEM" ], + "type": "talk_topic", + "responses": [ + { "text": "Combat commands...", "topic": "TALK_COMBAT_COMMANDS" }, + { "text": "Can I do anything for you?", "topic": "TALK_MISSION_LIST" }, + { + "text": "Can you teach me anything?", + "trial": { + "type": "CONDITION", + "condition": { + "or": [ + { "npc_need": "thirst", "amount": 80 }, + { "npc_need": "hunger", "amount": 160 }, + { "npc_need": "fatigue", "level": "TIRED" }, + { "npc_has_effect": "asked_to_train" } + ] + } + }, + "success": { "topic": "TALK_DENY_TRAIN" }, + "failure": { "topic": "TALK_TRAIN_PERSUADE" } + }, + { "text": "Let's trade items", "topic": "TALK_FRIEND", "effect": "start_trade" }, + { "text": "Guard this position", "topic": "TALK_FRIEND_GUARD", "effect": "assign_guard" }, + { "text": "I'd like to know a bit more about you...", "topic": "TALK_FRIEND", "effect": "reveal_stats" }, + { "text": "I want you to use this item", "topic": "TALK_USE_ITEM" }, + { "text": "Hold on to this item", "topic": "TALK_GIVE_ITEM" }, + { "text": "Miscellaneous rules...", "topic": "TALK_MISC_RULES" }, + { "text": "I'm going to go my own way for a while.", "topic": "TALK_LEAVE" }, + { "text": "Let's go.", "topic": "TALK_DONE" }, + { + "text": "Relax and chat with me for a while...", + "topic": "TALK_FRIEND_CHAT", + "condition": { "not": { "npc_has_effect": "asked_to_socialize" } } + }, + { "text": "Let's talk about faction camps.", "topic": "TALK_CAMP_GENERAL" } + ] + }, + { + "id": "TALK_COMBAT_COMMANDS", + "type": "talk_topic", + "dynamic_line": { + "and": [ + { + "npc_engagement_rule": "ENGAGE_NONE", + "no": { + "npc_engagement_rule": "ENGAGE_CLOSE", + "no": { + "npc_engagement_rule": "ENGAGE_WEAK", + "no": { + "npc_engagement_rule": "ENGAGE_HIT", + "no": { + "npc_engagement_rule": "ENGAGE_NO_MOVE", + "no": "*will engage all enemies.", + "yes": "*will engage enemies close enough to attack without moving." + }, + "yes": "*will engage enemies you attack." + }, + "yes": "*will engage weak enemies." + }, + "yes": "*will engage nearby enemies." + }, + "yes": "*will not engage enemies." + }, + { + "npc_rule": "use_guns", + "yes": { + "npc_rule": "use_silent", + "yes": " will use silenced ranged weapons.", + "no": " will use ranged weapons." + }, + "no": " will not use ranged weapons." + }, + { + "npc_rule": "use_grenades", + "yes": " will use grenades.", + "no": " will not use grenades." + }, + { + "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." + }, + " What should do?" + ] + }, + "responses": [ + { "text": "Change your engagement rules...", "topic": "TALK_COMBAT_ENGAGEMENT" }, + { "text": "Change your aiming rules...", "topic": "TALK_AIM_RULES" }, + { + "truefalsetext": { + "condition": { "npc_rule": "use_guns" }, + "true": "Don't use ranged weapons anymore.", + "false": "You can use ranged weapons." + }, + "topic": "TALK_COMBAT_COMMANDS", + "effect": { "toggle_npc_rule": "use_guns" } + }, + { + "truefalsetext": { "condition": { "npc_rule": "use_silent" }, "true": "Don't worry about noise.", "false": "Use only silent weapons." }, + "topic": "TALK_COMBAT_COMMANDS", + "effect": { "toggle_npc_rule": "use_silent" } + }, + { + "truefalsetext": { "condition": { "npc_rule": "use_grenades" }, "true": "Don't use grenades anymore.", "false": "You can use grenades." }, + "topic": "TALK_COMBAT_COMMANDS", + "effect": { "toggle_npc_rule": "use_grenades" } + }, + { + "truefalsetext": { + "condition": { "npc_rule": "avoid_friendly_fire" }, + "true": "Don't worry about shooting an ally.", + "false": "Don't shoot unless you're sure you won't hit an ally." + }, + "topic": "TALK_COMBAT_COMMANDS", + "effect": { "toggle_npc_rule": "avoid_friendly_fire" } + }, + { "text": "Never mind.", "topic": "TALK_NONE" } + ] + }, + { + "id": "TALK_COMBAT_ENGAGEMENT", + "type": "talk_topic", + "dynamic_line": { + "and": [ + { + "npc_engagement_rule": "ENGAGE_NONE", + "no": { + "npc_engagement_rule": "ENGAGE_CLOSE", + "no": { + "npc_engagement_rule": "ENGAGE_WEAK", + "no": { + "npc_engagement_rule": "ENGAGE_HIT", + "no": { + "npc_engagement_rule": "ENGAGE_NO_MOVE", + "no": "*will engage all enemies.", + "yes": "*will engage enemies close enough to attack without moving." + }, + "yes": "*will engage enemies you attack." + }, + "yes": "*will engage weak enemies." + }, + "yes": "*will engage nearby enemies." + }, + "yes": "*will not engage enemies." + }, + " What should do?" + ] + }, + "responses": [ + { + "text": "Don't fight unless your life depends on it.", + "topic": "TALK_NONE", + "condition": { "not": { "npc_engagement_rule": "ENGAGE_NONE" } }, + "effect": { "set_npc_engagement_rule": "ENGAGE_NONE" } + }, + { + "text": "Attack enemies that get too close.", + "topic": "TALK_NONE", + "condition": { "not": { "npc_engagement_rule": "ENGAGE_CLOSE" } }, + "effect": { "set_npc_engagement_rule": "ENGAGE_CLOSE" } + }, + { + "text": "Attack enemies that you can kill easily.", + "topic": "TALK_NONE", + "condition": { "not": { "npc_engagement_rule": "ENGAGE_WEAK" } }, + "effect": { "set_npc_engagement_rule": "ENGAGE_WEAK" } + }, + { + "text": "Attack only enemies that I attack first.", + "topic": "TALK_NONE", + "condition": { "not": { "npc_engagement_rule": "ENAGE_HIT" } }, + "effect": { "set_npc_engagement_rule": "ENGAGE_HIT" } + }, + { + "text": "Attack only enemies you can reach without moving.", + "topic": "TALK_NONE", + "condition": { "not": { "npc_engagement_rule": "ENGAGE_NO_MOVE" } }, + "effect": { "set_npc_engagement_rule": "ENGAGE_NO_MOVE" } + }, + { + "text": "Attack anything you want.", + "topic": "TALK_NONE", + "condition": { "not": { "npc_engagement_rule": "ENGAGE_ALL" } }, + "effect": { "set_npc_engagement_rule": "ENGAGE_ALL" } + }, + { "text": "Never mind.", "topic": "TALK_NONE" } + ] + }, + { + "id": "TALK_AIM_RULES", + "type": "talk_topic", + "dynamic_line": { + "and": [ + { + "npc_aim_rule": "AIM_WHEN_CONVENIENT", + "no": { + "npc_aim_rule": "AIM_STRICTLY_PRECISE", + "no": { "npc_aim_rule": "AIM_PRECISE", "no": "*will not bother to aim at all.", "yes": "*will take time and aim carefully." }, + "yes": "*will only shoot after taking a long time to aim." + }, + "yes": "*will aim when it's convenient." + }, + " How should aim?" + ] + }, + "responses": [ + { + "text": "Aim when it's convenient.", + "topic": "TALK_NONE", + "condition": { "not": { "npc_aim_rule": "AIM_WHEN_CONVENIENT" } }, + "effect": { "set_npc_aim_rule": "AIM_WHEN_CONVENIENT" } + }, + { + "text": "Go wild, you don't need to aim much.", + "topic": "TALK_NONE", + "condition": { "not": { "npc_aim_rule": "AIM_SPRAY" } }, + "effect": { "set_npc_aim_rule": "AIM_SPRAY" } + }, + { + "text": "Take your time, aim carefully.", + "topic": "TALK_NONE", + "condition": { "not": { "npc_aim_rule": "AIM_PRECISE" } }, + "effect": { "set_npc_aim_rule": "AIM_PRECISE" } + }, + { + "text": "Don't shoot if you can't aim really well.", + "topic": "TALK_NONE", + "condition": { "not": { "npc_aim_rule": "AIM_STRICTLY_PRECISE" } }, + "effect": { "set_npc_aim_rule": "AIM_STRICTLY_PRECISE" } + }, + { "text": "Never mind.", "topic": "TALK_NONE" } + ] + }, + { + "id": "TALK_TRAIN_PERSUADE", + "type": "talk_topic", + "dynamic_line": "Why should I teach you?", + "responses": [ + { + "text": "Come on, we're friends.", + "trial": { "type": "PERSUADE", "difficulty": 0, "mods": [ [ "TRUST", 3 ], [ "VALUE", 1 ], [ "ANGER", -3 ] ] }, + "success": { "topic": "TALK_TRAIN" }, + "failure": { "topic": "TALK_DENY_PERSONAL", "effect": { "npc_add_effect": "asked_to_train", "duration": 3600 } } + }, + { "text": "Never mind then.", "topic": "TALK_NONE" }, + { "text": "Forget it, let's get going.", "topic": "TALK_DONE" } + ] + }, + { + "id": "TALK_MISC_RULES", + "type": "talk_topic", + "dynamic_line": { + "and": [ + { + "npc_rule": "allow_pick_up", + "yes": { "has_pickup_list": true, "yes": "* will pick up items from the whitelist.", "no": "* will pick up all items." }, + "no": "* will not pick up items." + }, + { + "npc_rule": "allow_bash", + "yes": " will bash down obstacles.", + "no": " will not bash down obstacles." + }, + { + "npc_rule": "allow_sleep", + "yes": " will sleep when tired.", + "no": " will sleep only when exhausted." + }, + { + "npc_rule": "allow_complain", + "yes": " will complain about wounds and needs.", + "no": " will only complain in an emergency." + }, + { + "npc_rule": "allow_pulp", + "yes": " will smash nearby zombie corpses.", + "no": " will leave zombie corpses intact." + }, + { + "npc_rule": "close_doors", + "yes": " will close doors behind themselves.", + "no": " will leave doors open." + } + ] + }, + "responses": [ + { + "text": "Follow same rules as this follower.", + "topic": "TALK_MISC_RULES", + "condition": { "npc_allies": 2 }, + "effect": "copy_npc_rules" + }, + { + "truefalsetext": { "condition": { "npc_rule": "allow_pick_up" }, "true": "Don't pick up items.", "false": "You can pick up items now." }, + "topic": "TALK_MISC_RULES", + "effect": { "toggle_npc_rule": "allow_pick_up" } + }, + { + "truefalsetext": { "condition": { "npc_rule": "allow_bash" }, "true": "Don't bash obstacles.", "false": "You can bash obstacles." }, + "topic": "TALK_MISC_RULES", + "effect": { "toggle_npc_rule": "allow_bash" } + }, + { + "truefalsetext": { "condition": { "npc_rule": "allow_sleep" }, "true": "Stay awake.", "false": "Sleep when you feel tired." }, + "topic": "TALK_MISC_RULES", + "effect": { "toggle_npc_rule": "allow_sleep" } + }, + { + "truefalsetext": { "condition": { "npc_rule": "allow_complain" }, "true": "Stay quiet.", "false": "Tell me when you need something." }, + "topic": "TALK_MISC_RULES", + "effect": { "toggle_npc_rule": "allow_complain" } + }, + { + "truefalsetext": { "condition": { "npc_rule": "allow_pulp" }, "true": "Leave corpses alone.", "false": "Smash zombie corpses." }, + "topic": "TALK_MISC_RULES", + "effect": { "toggle_npc_rule": "allow_pulp" } + }, + { + "truefalsetext": { "condition": { "npc_rule": "close_doors" }, "true": "Leave doors open.", "false": "Close the doors." }, + "topic": "TALK_MISC_RULES", + "effect": { "toggle_npc_rule": "close_door" } + }, + { "text": "Set up pickup rules.", "topic": "TALK_MISC_RULES", "effect": "set_npc_pickup" }, + { "text": "Never mind.", "topic": "TALK_NONE" } + ] + }, + { + "id": "TALK_LEAVE", + "type": "talk_topic", + "dynamic_line": "You're really leaving?", + "responses": [ + { "text": "Yeah, I'm sure. Bye.", "topic": "TALK_DONE", "effect": "leave" }, + { "text": "Nah, I'm just kidding.", "topic": "TALK_NONE" } + ] + }, + { + "id": "TALK_FRIEND_CHAT", + "type": "talk_topic", + "dynamic_line": [ + { "u_has_item": "beer", "yes": "", "no": "" }, + { "u_has_item": "european_pilsner", "yes": "", "no": "" }, + { "u_has_item": "pale_ale", "yes": "", "no": "" }, + { "u_has_item": "india_pale_ale", "yes": "", "no": "" }, + { + "is_season": "summer", + "yes": "Yeah, this summer heat is hitting me hard, let's take a quick break, how goes it ?", + "no": "" + }, + { + "is_season": "winter", + "yes": "OK, maybe it'll stop me from freezing in this weather, what's up?", + "no": "" + }, + { + "is_day": "Well, it's the time of day for a quick break surely! How are you holding up?", + "is_night": "Man it's dark out isn't it? what's up?" + }, + { + "npc_has_effect": "infected", + "yes": "Well, I'm feeling pretty sick... are you doing OK though?", + "no": "" + }, + { + "u_has_mission": true, + "many": "Definitely, by the way, thanks for helping me so much with my tasks! Anyway, you coping OK, ? ", + "one": "OK, let's take a moment, oh, and thanks for helping me with that thing, so... what's up?", + "none": "" + }, + { + "days_since_cataclysm": 30, + "yes": "Now, we've got a moment, I was just thinking it's been a month or so since... since all this, how are you coping with it all?", + "no": "" + } + ], + "responses": [ + { + "text": "Oh you know, not bad, not bad...", + "topic": "TALK_DONE", + "switch": true, + "effect": [ "morale_chat_activity", { "npc_add_effect": "asked_to_socialize", "duration": 7000 } ] + } + ] + } +] diff --git a/data/json/npcs/TALK_COMMON_OTHER.json b/data/json/npcs/TALK_COMMON_OTHER.json index 950bcd44babfe..7d1f096a9d21c 100644 --- a/data/json/npcs/TALK_COMMON_OTHER.json +++ b/data/json/npcs/TALK_COMMON_OTHER.json @@ -13,65 +13,6 @@ { "text": "Well, bye.", "topic": "TALK_DONE" } ] }, - { - "id": "TALK_FRIEND", - "type": "talk_topic", - "responses": [ - { - "text": "Relax and chat with me for a while...", - "topic": "TALK_FRIEND_CHAT", - "condition": { "not": { "npc_has_effect": "asked_to_socialize" } } - } - ] - }, - { - "id": "TALK_FRIEND_CHAT", - "type": "talk_topic", - "dynamic_line": [ - { "u_has_item": "beer", "yes": "", "no": "" }, - { "u_has_item": "european_pilsner", "yes": "", "no": "" }, - { "u_has_item": "pale_ale", "yes": "", "no": "" }, - { "u_has_item": "india_pale_ale", "yes": "", "no": "" }, - { - "is_season": "summer", - "yes": "Yeah, this summer heat is hitting me hard, let's take a quick break, how goes it ?", - "no": "" - }, - { - "is_season": "winter", - "yes": "OK, maybe it'll stop me from freezing in this weather, what's up?", - "no": "" - }, - { - "is_day": "Well, it's the time of day for a quick break surely! How are you holding up?", - "is_night": "Man it's dark out isn't it? what's up?" - }, - { - "npc_has_effect": "infected", - "yes": "Well, I'm feeling pretty sick... are you doing OK though?", - "no": "" - }, - { - "u_has_mission": true, - "many": "Definitely, by the way, thanks for helping me so much with my tasks! Anyway, you coping OK, ? ", - "one": "OK, let's take a moment, oh, and thanks for helping me with that thing, so... what's up?", - "none": "" - }, - { - "days_since_cataclysm": 30, - "yes": "Now, we've got a moment, I was just thinking it's been a month or so since... since all this, how are you coping with it all?", - "no": "" - } - ], - "responses": [ - { - "text": "Oh you know, not bad, not bad...", - "topic": "TALK_DONE", - "switch": true, - "effect": [ "morale_chat_activity", { "npc_add_effect": "asked_to_socialize", "duration": 7000 } ] - } - ] - }, { "id": "TALK_SHELTER_PLANS", "type": "talk_topic", @@ -168,15 +109,6 @@ "dynamic_line": "Yeah... I don't think so.", "responses": [ { "text": "Oh, okay.", "topic": "TALK_DONE" } ] }, - { - "id": "TALK_LEAVE", - "type": "talk_topic", - "dynamic_line": "You're really leaving?", - "responses": [ - { "text": "Yeah, I'm sure. Bye.", "topic": "TALK_DONE", "effect": "leave" }, - { "text": "Nah, I'm just kidding.", "topic": "TALK_NONE" } - ] - }, { "id": "TALK_LEADER", "type": "talk_topic", @@ -230,7 +162,7 @@ "dynamic_line": "I'm on watch.", "responses": [ { "text": "I need you to come with me.", "topic": "TALK_FRIEND", "effect": "stop_guard" }, - { "text": "See you around.", "topic": "TALK_NONE" } + { "text": "See you around.", "topic": "TALK_DONE" } ] }, { diff --git a/data/json/npcs/TALK_TEST.json b/data/json/npcs/TALK_TEST.json index 79f896e8a937d..8799b04421d65 100644 --- a/data/json/npcs/TALK_TEST.json +++ b/data/json/npcs/TALK_TEST.json @@ -175,6 +175,25 @@ { "text": "This an error! npc allies 2 test response.", "topic": "TALK_DONE", "condition": { "npc_allies": 2 } } ] }, + { + "type": "talk_topic", + "id": "TALK_TEST_NPC_RULES", + "dynamic_line": "This is a test conversation that shouldn't appear in the game.", + "responses": [ + { "text": "This is a basic test response.", "topic": "TALK_DONE" }, + { + "text": "This is a npc engagement rule test response.", + "topic": "TALK_DONE", + "condition": { "npc_engagement_rule": "ENGAGE_ALL" } + }, + { + "text": "This is a npc aim rule test response.", + "topic": "TALK_DONE", + "condition": { "npc_aim_rule": "AIM_SPRAY" } + }, + { "text": "This is a npc rule test response.", "topic": "TALK_DONE", "condition": { "npc_rule": "use_silent" } } + ] + }, { "type": "talk_topic", "id": "TALK_TEST_NPC_NEEDS", diff --git a/doc/NPCs.md b/doc/NPCs.md index 3de2737194f9d..25a9e5c38602e 100644 --- a/doc/NPCs.md +++ b/doc/NPCs.md @@ -319,6 +319,33 @@ The dynamic line is chosen if the player is driving a vehicle, or the NPC is dri } ``` +#### Based on an NPC follower AI rule +The dynamic line is chosen based on NPC follower AI rules settings. There are three variants: `npc_aim_rule`, `npc_engagement_rule`, and `npc_rule`, all of which take a rule value and an optional `yes` and `no` response. The `yes` response is chosen if the NPC follower AI rule value matches the rule value and otherwise the no value is chosen. + +`npc_aim_rule` values are currently "AIM_SPRAY", "AIM_WHEN_CONVENIENT", "AIM_PRECISE", or "AIM_STRICTLY_PRECISE". +`npc_engagement_rule` values are currently "ENGAGE_NONE", "ENGAGE_CLOSE", "ENGAGE_WEAK", "ENGAGE_HIT", or "ENGAGE_NO_MOVE". +`npc_rule` values are currently "use_guns", "use_grenades", "use_silent", "avoid_friendly_fire", "allow_pick_up", "allow_bash", "allow_sleep", "allow_complain", "allow_pulp", or "close_doors". + +```C++ +{ + "and": [ + { + "npc_aim_rule": "AIM_STRICTLY_PRECISE", + "yes": "No wasting ammo, got it. " + }, + { + "npc_engagement_rule": "ENGAGE_NO_MOVE", + "yes": "Stay where I am. " + }, + { + "npc_rule": "allow_pulp", + "yes": "Pulp the corpses when I'm done.", + "no": "Leave the corpses for someone else to deal with." + } + ] +} +``` + #### Based on whether the NPC has a pickup list The dynamic line is chosen based on whether the NPC has a pickup list or not. The line from `yes` will be shown if they have a pickup list and otherwise the line from `no`. The line from `yes` will be shown even if `npc_rule`: `allow_pick_up` is false. @@ -582,6 +609,9 @@ bionic_install | The NPC installs a bionic from your character's inventory onto bionic_remove | The NPC removes a bionic from your character, using very high skill, and charging you according to the operation's difficulty. npc_faction_change: faction_string | Change the NPC's faction membership to `faction_string`. u_faction_rep: rep_num | Increase's your reputation with the NPC's current faction, or decreases it if `rep_num` is negative. +toggle_npc_rule: rule_string | Toggles the value of a boolean NPC follower AI rule such as "use_silent" or "allow_bash" +set_npc_engagement_rule: rule_string | Sets the NPC follower AI rule for engagement distance to the value of `rule_string`. +set_npc_aim_rule: rule_string | Sets the NPC follower AI rule for aiming speed to the value of `rule_string`. #### Deprecated @@ -683,6 +713,13 @@ Condition | Type | Description "npc_role_nearby" | string | `true` if there is an NPC with the same companion mission role as `npc_role_nearby` within 100 tiles. "npc_has_weapon" | simple string | `true` if the NPC is wielding a weapon. +#### NPC Follower AI rules +Condition | Type | Description +--- | --- | --- +"npc_aim_rule" | string | `true` if the NPC follower AI rule for aiming matches the string. +"npc_engagement_rule" | string | `true` if the NPC follower AI rule for engagement matches the string. +"npc_rule" | string | `true` if the NPC follower AI rule for that matches string is set. + #### Environment Condition | Type | Description @@ -692,6 +729,7 @@ Condition | Type | Description "is_day" | simple string | `true` if it is currently daytime. "is_outside" | simple string | `true` if the NPC is on a tile without a roof. + #### Sample responses with conditions ```C++ { diff --git a/src/dialogue.h b/src/dialogue.h index 34e954cf64496..b15dbf491fdd0 100644 --- a/src/dialogue.h +++ b/src/dialogue.h @@ -105,6 +105,10 @@ struct talk_effect_fun_t { void set_npc_change_faction( const std::string &faction_name ); void set_change_faction_rep( int amount ); void set_add_debt( const std::vector debt_modifiers ); + void set_toggle_npc_rule( const std::string &rule ); + void set_npc_engagement_rule( const std::string &setting ); + void set_npc_aim_rule( const std::string &setting ); + void operator()( const dialogue &d ) const { if( !function ) { return; diff --git a/src/npc.cpp b/src/npc.cpp index 60e916b6a2b47..dd5ee58ff07aa 100644 --- a/src/npc.cpp +++ b/src/npc.cpp @@ -1655,7 +1655,7 @@ Creature::Attitude npc::attitude_to( const Creature &other ) const int npc::smash_ability() const { - if( !is_following() || rules.allow_bash ) { + if( !is_following() || rules.has_flag( ally_rule::allow_bash ) ) { ///\EFFECT_STR_NPC increases smash ability return str_cur + weapon.damage_melee( DT_BASH ); } @@ -2439,3 +2439,44 @@ void npc::set_attitude( npc_attitude new_attitude ) } attitude = new_attitude; } + +npc_follower_rules::npc_follower_rules() +{ + engagement = ENGAGE_CLOSE; + aim = AIM_WHEN_CONVENIENT; + set_flag( ally_rule::use_guns ); + set_flag( ally_rule::use_grenades ); + clear_flag( ally_rule::use_silent ); + set_flag( ally_rule::avoid_friendly_fire ); + + clear_flag( ally_rule::allow_pick_up ); + clear_flag( ally_rule::allow_bash ); + clear_flag( ally_rule::allow_sleep ); + set_flag( ally_rule::allow_complain ); + set_flag( ally_rule::allow_pulp ); + clear_flag( ally_rule::close_doors ); +} + +bool npc_follower_rules::has_flag( ally_rule test ) const +{ + return static_cast( test ) & static_cast( flags ); +} + +void npc_follower_rules::set_flag( ally_rule setit ) +{ + flags = static_cast( static_cast( flags ) | static_cast( setit ) ); +} + +void npc_follower_rules::clear_flag( ally_rule clearit ) +{ + flags = static_cast( static_cast( flags ) & ~static_cast( clearit ) ); +} + +void npc_follower_rules::toggle_flag( ally_rule toggle ) +{ + if( has_flag( toggle ) ) { + clear_flag( toggle ); + } else { + set_flag( toggle ); + } +} diff --git a/src/npc.h b/src/npc.h index 628e0538006e9..62aaf18d8d387 100644 --- a/src/npc.h +++ b/src/npc.h @@ -179,6 +179,16 @@ enum combat_engagement { ENGAGE_ALL, ENGAGE_NO_MOVE }; +const std::unordered_map combat_engagement_strs = { { + { "ENGAGE_NONE", ENGAGE_NONE }, + { "ENGAGE_CLOSE", ENGAGE_CLOSE }, + { "ENGAGE_WEAK", ENGAGE_WEAK }, + { "ENGAGE_HIT", ENGAGE_HIT }, + { "ENGAGE_ALL", ENGAGE_ALL }, + { "ENGAGE_NO_MOVE", ENGAGE_NO_MOVE } + } +}; + enum aim_rule { // Aim some @@ -190,19 +200,45 @@ enum aim_rule { // If you can't aim, don't shoot AIM_STRICTLY_PRECISE }; +const std::unordered_map aim_rule_strs = { { + { "AIM_WHEN_CONVENIENT", AIM_WHEN_CONVENIENT }, + { "AIM_SPRAY", AIM_SPRAY }, + { "AIM_PRECISE", AIM_PRECISE }, + { "AIM_STRICTLY_PRECISE", AIM_STRICTLY_PRECISE } + } +}; + +enum class ally_rule { + DEFAULT = 0, + use_guns = 1, + use_grenades = 2, + use_silent = 4, + avoid_friendly_fire = 8, + allow_pick_up = 16, + allow_bash = 32, + allow_sleep = 64, + allow_complain = 128, + allow_pulp = 256, + close_doors = 512 +}; +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 } + } +}; struct npc_follower_rules { combat_engagement engagement; aim_rule aim = AIM_WHEN_CONVENIENT; - bool use_guns; - bool use_grenades; - bool use_silent; - - bool allow_pick_up; - bool allow_bash; - bool allow_sleep; - bool allow_complain; - bool allow_pulp; + ally_rule flags; bool close_doors; @@ -212,6 +248,12 @@ struct npc_follower_rules { void serialize( JsonOut &jsout ) const; void deserialize( JsonIn &jsin ); + + bool has_flag( ally_rule test ) const; + void set_flag( ally_rule setit ); + void clear_flag( ally_rule clearit ); + void toggle_flag( ally_rule toggle ); + }; // Data relevant only for this action diff --git a/src/npcmove.cpp b/src/npcmove.cpp index e8a004b25d3a8..6ceed2b22e210 100644 --- a/src/npcmove.cpp +++ b/src/npcmove.cpp @@ -966,7 +966,7 @@ npc_action npc::method_of_attack() // if we require a silent weapon inappropriate modes are also removed // except in emergency only fire bursts if danger > 0.5 and don't shoot at all at harmless targets std::vector> modes; - if( rules.use_guns || !is_following() ) { + if( rules.has_flag( ally_rule::use_guns ) || !is_following() ) { for( const auto &e : weapon.gun_all_modes() ) { modes.push_back( e ); } @@ -979,7 +979,8 @@ npc_action npc::method_of_attack() !m->ammo_sufficient( m.qty ) || !can_use( *m.target ) || m->get_gun_ups_drain() > ups_charges || ( danger <= ( ( m.qty == 1 ) ? 0.0 : 0.5 ) && !emergency() ) || - ( rules.use_silent && is_following() && !m.target->is_silent() ); + ( rules.has_flag( ally_rule::use_silent ) && is_following() && + !m.target->is_silent() ); } ), modes.end() ); } @@ -1209,7 +1210,7 @@ npc_action npc::address_needs( float danger ) return npc_undecided; } - if( rules.allow_sleep || get_fatigue() > MASSIVE_FATIGUE ) { + if( rules.has_flag( ally_rule::allow_sleep ) || get_fatigue() > MASSIVE_FATIGUE ) { return npc_sleep; } else if( g->u.in_sleep_state() ) { // TODO: "Guard me while I sleep" command @@ -1882,7 +1883,7 @@ void npc::move_away_from( const std::vector &spheres, bool no_bashing ) void npc::find_item() { - if( is_following() && !rules.allow_pick_up ) { + if( is_following() && !rules.has_flag( ally_rule::allow_pick_up ) ) { // Grabbing stuff not allowed by our "owner" return; } @@ -2017,7 +2018,7 @@ void npc::find_item() void npc::pick_up_item() { - if( is_following() && !rules.allow_pick_up ) { + if( is_following() && !rules.has_flag( ally_rule::allow_pick_up ) ) { add_msg( m_debug, "%s::pick_up_item(); Cancelling on player's request", name.c_str() ); fetching_item = false; moves -= 1; @@ -2275,7 +2276,7 @@ void npc::drop_items( int weight, int volume ) bool npc::find_corpse_to_pulp() { - if( is_following() && ( !rules.allow_pulp || g->u.in_vehicle ) ) { + if( is_following() && ( !rules.has_flag( ally_rule::allow_pulp ) || g->u.in_vehicle ) ) { return false; } @@ -2371,8 +2372,8 @@ bool npc::do_pulp() bool npc::wield_better_weapon() { // TODO: Allow wielding weaker weapons against weaker targets - bool can_use_gun = ( !is_following() || rules.use_guns ); - bool use_silent = ( is_following() && rules.use_silent ); + bool can_use_gun = ( !is_following() || rules.has_flag( ally_rule::use_guns ) ); + bool use_silent = ( is_following() && rules.has_flag( ally_rule::use_silent ) ); invslice slice = inv.slice(); // Check if there's something better to wield @@ -2486,7 +2487,7 @@ void npc_throw( npc &np, item &it, int index, const tripoint &pos ) bool npc::alt_attack() { - if( is_following() && !rules.use_grenades ) { + if( is_following() && !rules.has_flag( ally_rule::use_grenades ) ) { return false; } @@ -3251,8 +3252,8 @@ bool npc::complain_about( const std::string &issue, const time_duration &dur, // Don't wake player up with non-serious complaints // Stop complaining while asleep - const bool do_complain = force || ( rules.allow_complain && !g->u.in_sleep_state() && - !in_sleep_state() ); + const bool do_complain = force || ( rules.has_flag( ally_rule::allow_complain ) && + !g->u.in_sleep_state() && !in_sleep_state() ); if( complain_since( issue, dur ) && do_complain ) { say( speech ); diff --git a/src/npctalk.cpp b/src/npctalk.cpp index 1d06e8ad56780..08e640fbff419 100644 --- a/src/npctalk.cpp +++ b/src/npctalk.cpp @@ -236,7 +236,7 @@ void game::chat() u.shout( _( "Stay awake!" ) ); } else if( nmenu.ret == yell_sleep ) { for( npc *p: followers ) { - p->rules.allow_sleep = true; + p->rules.set_flag( ally_rule::allow_sleep ); } u.shout( _( "We're safe! Take a nap if you're tired." ) ); } else if( nmenu.ret == yell_follow ) { @@ -549,53 +549,6 @@ std::string dialogue::dynamic_line( const talk_topic &the_topic ) const } else if( topic == "TALK_DENY_TRAIN" ) { return the_topic.reason; - } else if( topic == "TALK_COMBAT_COMMANDS" ) { - std::stringstream status; - // Prepending * makes this an action, not a phrase - switch( p->rules.engagement ) { - case ENGAGE_NONE: - status << _( "*is not engaging enemies." ); - break; - case ENGAGE_CLOSE: - status << _( "*is engaging nearby enemies." ); - break; - case ENGAGE_WEAK: - status << _( "*is engaging weak enemies." ); - break; - case ENGAGE_HIT: - status << _( "*is engaging enemies you attack." ); - break; - case ENGAGE_NO_MOVE: - status << _( "*is engaging enemies close enough to attack without moving." ); - break; - case ENGAGE_ALL: - status << _( "*is engaging all enemies." ); - break; - } - std::string npcstr = p->male ? pgettext( "npc", "He" ) : pgettext( "npc", "She" ); - if( p->rules.use_guns ) { - if( p->rules.use_silent ) { - status << string_format( _( " %s will use silenced firearms." ), npcstr ); - } else { - status << string_format( _( " %s will use firearms." ), npcstr ); - } - } else { - status << string_format( _( " %s will not use firearms." ), npcstr ); - } - if( p->rules.use_grenades ) { - status << string_format( _( " %s will use grenades." ), npcstr ); - } else { - status << string_format( _( " %s will not use grenades." ), npcstr ); - } - - return status.str(); - - } else if( topic == "TALK_COMBAT_ENGAGEMENT" ) { - return _( "What should I do?" ); - - } else if( topic == "TALK_AIM_RULES" ) { - return _( "How should I aim?" ); - } else if( topic == "TALK_DESCRIBE_MISSION" ) { switch( p->mission ) { case NPC_MISSION_SHELTER: @@ -699,65 +652,6 @@ std::string dialogue::dynamic_line( const talk_topic &the_topic ) const return "&" + p->short_description(); } else if( topic == "TALK_OPINION" ) { return "&" + p->opinion_text(); - } else if( topic == "TALK_WAKE_UP" ) { - if( p->has_effect( effect_sleep ) ) { - if( p->get_fatigue() > EXHAUSTED ) { - return _( "No, just no..." ); - } else if( p->get_fatigue() > DEAD_TIRED ) { - return _( "Just let me sleep, !" ); - } else if( p->get_fatigue() > TIRED ) { - return _( "Make it quick, I want to go back to sleep." ); - } else { - return _( "Just few minutes more..." ); - } - } else { - return _( "Anything to do before I go to sleep?" ); - } - - } else if( topic == "TALK_MISC_RULES" ) { - std::stringstream status; - std::string npcstr = p->male ? pgettext( "npc", "He" ) : pgettext( "npc", "She" ); - - if( p->rules.allow_pick_up && p->rules.pickup_whitelist->empty() ) { - status << string_format( _( " %s will pick up all items." ), npcstr ); - } else if( p->rules.allow_pick_up ) { - status << string_format( _( " %s will pick up items from the whitelist." ), npcstr ); - } else { - status << string_format( _( " %s will not pick up items." ), npcstr ); - } - - if( p->rules.allow_bash ) { - status << string_format( _( " %s will bash down obstacles." ), npcstr ); - } else { - status << string_format( _( " %s will not bash down obstacles." ), npcstr ); - } - - if( p->rules.allow_sleep ) { - status << string_format( _( " %s will sleep when tired." ), npcstr ); - } else { - status << string_format( _( " %s will sleep only when exhausted." ), npcstr ); - } - - if( p->rules.allow_complain ) { - status << string_format( _( " %s will complain about wounds and needs." ), npcstr ); - } else { - status << string_format( _( " %s will only complain in an emergency." ), npcstr ); - } - - if( p->rules.allow_pulp ) { - status << string_format( _( " %s will smash nearby zombie corpses." ), npcstr ); - } else { - status << string_format( _( " %s will leave zombie corpses intact." ), npcstr ); - } - - if( p->rules.close_doors ) { - status << string_format( _( " %s will close doors behind themselves." ), npcstr ); - } else { - status << string_format( _( " %s will leave doors open." ), npcstr ); - } - - return status.str(); - } else if( topic == "TALK_USE_ITEM" ) { return give_item_to( *p, true, false ); } else if( topic == "TALK_GIVE_ITEM" ) { @@ -1102,209 +996,10 @@ void dialogue::gen_responses( const talk_topic &the_topic ) } else if( topic == "TALK_HOW_MUCH_FURTHER" ) { add_response_none( _( "Okay, thanks." ) ); add_response_done( _( "Let's keep moving." ) ); - - } else if( topic == "TALK_FRIEND" || topic == "TALK_GIVE_ITEM" || topic == "TALK_USE_ITEM" ) { - if( p->is_following() ) { - add_response( _( "Combat commands..." ), "TALK_COMBAT_COMMANDS" ); - } - - add_response( _( "Can I do anything for you?" ), "TALK_MISSION_LIST" ); - if( p->is_following() ) { - // TODO: Allow NPCs to break training properly - // Don't allow them to walk away in the middle of training - std::stringstream reasons; - if( const optional_vpart_position vp = g->m.veh_at( p->pos() ) ) { - if( abs( vp->vehicle().velocity ) > 0 ) { - reasons << _( "I can't train you properly while you're operating a vehicle!" ) << std::endl; - } - } - - if( p->has_effect( effect_asked_to_train ) ) { - reasons << _( "Give it some time, I'll show you something new later..." ) << std::endl; - } - - if( p->get_thirst() > 80 ) { - reasons << _( "I'm too thirsty, give me something to drink." ) << std::endl; - } - - if( p->get_hunger() > 160 ) { - reasons << _( "I'm too hungry, give me something to eat." ) << std::endl; - } - - if( p->get_fatigue() > TIRED ) { - reasons << _( "I'm too tired, let me rest first." ) << std::endl; - } - - if( !reasons.str().empty() ) { - RESPONSE( _( "[N/A] Can you teach me anything?" ) ); - SUCCESS( "TALK_DENY_TRAIN" ); - ret.back().success.next_topic.reason = reasons.str(); - } else { - RESPONSE( _( "Can you teach me anything?" ) ); - int commitment = 3 * p->op_of_u.trust + 1 * p->op_of_u.value - - 3 * p->op_of_u.anger; - TRIAL( TALK_TRIAL_PERSUADE, commitment * 2 ); - SUCCESS( "TALK_TRAIN" ); - SUCCESS_ACTION( []( npc &p ) { p.chatbin.mission_selected = nullptr; } ); - FAILURE( "TALK_DENY_PERSONAL" ); - FAILURE_ACTION( &talk_function::deny_train ); - } - } - add_response( _( "Let's trade items." ), "TALK_FRIEND", &talk_function::start_trade ); - if( p->is_following() && g->m.camp_at( g->u.pos() ) ) { - add_response( _( "Wait at this base." ), "TALK_DONE", &talk_function::assign_base ); - } - if( p->is_following() ) { - RESPONSE( _( "Guard this position." ) ); - SUCCESS( "TALK_FRIEND_GUARD" ); - SUCCESS_ACTION( &talk_function::assign_guard ); - - RESPONSE( _( "I'd like to know a bit more about you..." ) ); - SUCCESS( "TALK_FRIEND" ); - SUCCESS_ACTION( &talk_function::reveal_stats ); - - add_response( _( "I want you to use this item" ), "TALK_USE_ITEM" ); - add_response( _( "Hold on to this item" ), "TALK_GIVE_ITEM" ); - add_response( _( "Miscellaneous rules..." ), "TALK_MISC_RULES" ); - - add_response( _( "I'm going to go my own way for a while." ), "TALK_LEAVE" ); - add_response_done( _( "Let's go." ) ); - - add_response( _( "Let's talk about faction camps." ), "TALK_CAMP_GENERAL" ); - } - - if( !p->is_following() ) { - add_response( _( "I need you to come with me." ), "TALK_FRIEND", - &talk_function::stop_guard ); - add_response_done( _( "Bye." ) ); - } - - } else if( topic == "TALK_COMBAT_COMMANDS" ) { - add_response( _( "Change your engagement rules..." ), "TALK_COMBAT_ENGAGEMENT" ); - add_response( _( "Change your aiming rules..." ), "TALK_AIM_RULES" ); - add_response( p->rules.use_guns ? _( "Don't use guns anymore." ) : - _( "You can use guns." ), - "TALK_COMBAT_COMMANDS", []( npc & np ) { - np.rules.use_guns = !np.rules.use_guns; - } ); - add_response( p->rules.use_silent ? _( "Don't worry about noise." ) : - _( "Use only silent weapons." ), - "TALK_COMBAT_COMMANDS", []( npc & np ) { - np.rules.use_silent = !np.rules.use_silent; - } ); - add_response( p->rules.use_grenades ? _( "Don't use grenades anymore." ) : - _( "You can use grenades." ), - "TALK_COMBAT_COMMANDS", []( npc & np ) { - np.rules.use_grenades = !np.rules.use_grenades; - } ); - add_response_none( _( "Never mind." ) ); - - } else if( topic == "TALK_COMBAT_ENGAGEMENT" ) { - struct engagement_setting { - combat_engagement rule; - std::string description; - }; - static const std::vector engagement_settings = {{ - { ENGAGE_NONE, _( "Don't fight unless your life depends on it." ) }, - { ENGAGE_CLOSE, _( "Attack enemies that get too close." ) }, - { ENGAGE_WEAK, _( "Attack enemies that you can kill easily." ) }, - { ENGAGE_HIT, _( "Attack only enemies that I attack first." ) }, - { ENGAGE_NO_MOVE, _( "Attack only enemies you can reach without moving." ) }, - { ENGAGE_ALL, _( "Attack anything you want." ) }, - } - }; - - for( const auto &setting : engagement_settings ) { - if( p->rules.engagement == setting.rule ) { - continue; - } - - combat_engagement eng = setting.rule; - add_response( setting.description, "TALK_NONE", [eng]( npc & np ) { - np.rules.engagement = eng; - }, dialogue_consequence::none ); - } - add_response_none( _( "Never mind." ) ); - - } else if( topic == "TALK_AIM_RULES" ) { - struct aim_setting { - aim_rule rule; - std::string description; - }; - static const std::vector aim_settings = {{ - { AIM_WHEN_CONVENIENT, _( "Aim when it's convenient." ) }, - { AIM_SPRAY, _( "Go wild, you don't need to aim much." ) }, - { AIM_PRECISE, _( "Take your time, aim carefully." ) }, - { AIM_STRICTLY_PRECISE, _( "Don't shoot if you can't aim really well." ) }, - } - }; - - for( const auto &setting : aim_settings ) { - if( p->rules.aim == setting.rule ) { - continue; - } - - aim_rule ar = setting.rule; - add_response( setting.description, "TALK_NONE", [ar]( npc & np ) { - np.rules.aim = ar; - }, dialogue_consequence::none ); - } - add_response_none( _( "Never mind." ) ); - } else if( topic == "TALK_SIZE_UP" || topic == "TALK_LOOK_AT" || topic == "TALK_OPINION" || topic == "TALK_SHOUT" ) { add_response_none( _( "Okay." ) ); - } else if( topic == "TALK_WAKE_UP" ) { - add_response( _( "Wake up!" ), "TALK_NONE", &talk_function::wake_up ); - add_response_done( _( "Go back to sleep." ) ); - - } else if( topic == "TALK_MISC_RULES" ) { - add_response( _( "Follow same rules as this follower." ), - "TALK_MISC_RULES", []( npc & np ) { - const npc *other = pick_follower(); - if( other != nullptr && other != &np ) { - np.rules = other->rules; - } - } ); - add_response( p->rules.allow_pick_up ? _( "Don't pick up items." ) : - _( "You can pick up items now." ), - "TALK_MISC_RULES", []( npc & np ) { - np.rules.allow_pick_up = !np.rules.allow_pick_up; - } ); - add_response( p->rules.allow_bash ? _( "Don't bash obstacles." ) : - _( "You can bash obstacles." ), - "TALK_MISC_RULES", []( npc & np ) { - np.rules.allow_bash = !np.rules.allow_bash; - } ); - add_response( p->rules.allow_sleep ? _( "Stay awake." ) : - _( "Sleep when you feel tired." ), - "TALK_MISC_RULES", []( npc & np ) { - np.rules.allow_sleep = !np.rules.allow_sleep; - } ); - add_response( p->rules.allow_complain ? _( "Stay quiet." ) : - _( "Tell me when you need something." ), - "TALK_MISC_RULES", []( npc & np ) { - np.rules.allow_complain = !np.rules.allow_complain; - } ); - add_response( p->rules.allow_pulp ? _( "Leave corpses alone." ) : - _( "Smash zombie corpses." ), - "TALK_MISC_RULES", []( npc & np ) { - np.rules.allow_pulp = !np.rules.allow_pulp; - } ); - add_response( p->rules.close_doors ? _( "Leave doors open." ) : - _( "Close the doors." ), - "TALK_MISC_RULES", []( npc & np ) { - np.rules.close_doors = !np.rules.close_doors; - } ); - - add_response( _( "Set up pickup rules." ), "TALK_MISC_RULES", []( npc & np ) { - const std::string title = string_format( _( "Pickup rules for %s" ), np.name ); - np.rules.pickup_whitelist->show( title, false ); - } ); - - add_response_none( _( "Never mind." ) ); - } if( g->u.has_trait( trait_DEBUG_MIND_CONTROL ) && !p->is_friend() ) { @@ -1685,6 +1380,9 @@ void parse_tags( std::string &phrase, const player &u, const player &me ) phrase.replace( fa, l, pgettext( "punctuation", "!" ) ); break; } + } else if( tag == "" ) { + std::string npcstr = me.male ? pgettext( "npc", "He" ) : pgettext( "npc", "She" ); + phrase.replace( fa, l, npcstr ); } else if( !tag.empty() ) { debugmsg( "Bad tag. '%s' (%d - %d)", tag.c_str(), fa, fb ); phrase.replace( fa, fb - fa + 1, "????" ); @@ -2080,6 +1778,37 @@ void talk_effect_fun_t::set_add_debt( const std::vector debt_modifier }; } +void talk_effect_fun_t::set_toggle_npc_rule( const std::string &rule ) +{ + function = [rule]( const dialogue &d ) { + auto toggle = ally_rule_strs.find( rule ); + if( toggle == ally_rule_strs.end() ) { + return; + } + d.beta->rules.toggle_flag( toggle->second ); + }; +} + +void talk_effect_fun_t::set_npc_engagement_rule( const std::string &setting ) +{ + function = [setting]( const dialogue &d ) { + auto rule = combat_engagement_strs.find( setting ); + if( rule != combat_engagement_strs.end() ) { + d.beta->rules.engagement = rule->second; + } + }; +} + +void talk_effect_fun_t::set_npc_aim_rule( const std::string &setting ) +{ + function = [setting]( const dialogue &d ) { + auto rule = aim_rule_strs.find( setting ); + if( rule != aim_rule_strs.end() ) { + d.beta->rules.aim = rule->second; + } + }; +} + void talk_effect_t::set_effect_consequence( const talk_effect_fun_t &fun, dialogue_consequence con ) { effects.push_back( fun ); @@ -2261,6 +1990,15 @@ void talk_effect_t::parse_sub_effect( JsonObject jo ) debt_modifiers.push_back( this_modifier ); } subeffect_fun.set_add_debt( debt_modifiers ); + } else if( jo.has_string( "toggle_npc_rule" ) ) { + const std::string rule = jo.get_string( "toggle_npc_rule" ); + subeffect_fun.set_toggle_npc_rule( rule ); + } else if( jo.has_string( "set_npc_engagement_rule" ) ) { + const std::string setting = jo.get_string( "set_npc_engagement_rule" ); + subeffect_fun.set_npc_engagement_rule( setting ); + } else if( jo.has_string( "set_npc_aim_rule" ) ) { + const std::string setting = jo.get_string( "set_npc_aim_rule" ); + subeffect_fun.set_npc_aim_rule( setting ); } else { jo.throw_error( "invalid sub effect syntax :" + jo.str() ); } @@ -2667,6 +2405,33 @@ conditional_t::conditional_t( JsonObject jo ) condition = [min_cash]( const dialogue & d ) { return d.alpha->cash >= min_cash; }; + } else if( jo.has_string( "npc_aim_rule" ) ) { + std::string setting = jo.get_string( "npc_aim_rule" ); + condition = [setting]( const dialogue &d ) { + auto rule = aim_rule_strs.find( setting ); + if( rule != aim_rule_strs.end() ) { + return d.beta->rules.aim == rule->second; + } + return false; + }; + } else if( jo.has_string( "npc_engagement_rule" ) ) { + std::string setting = jo.get_string( "npc_engagement_rule" ); + condition = [setting]( const dialogue &d ) { + auto rule = combat_engagement_strs.find( setting ); + if( rule != combat_engagement_strs.end() ) { + return d.beta->rules.engagement == rule->second; + } + return false; + }; + } else if( jo.has_string( "npc_rule" ) ) { + std::string rule = jo.get_string( "npc_rule" ); + 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 false; + }; } else if( jo.has_int( "days_since_cataclysm" ) ) { const unsigned long days = jo.get_int( "days_since_cataclysm" ); condition = [days]( const dialogue & ) { @@ -3123,6 +2888,40 @@ dynamic_line_t::dynamic_line_t( JsonObject jo ) } return no_vehicle( d ); }; + } else if( jo.has_string( "npc_aim_rule" ) ) { + std::string setting = jo.get_string( "npc_aim_rule" ); + const dynamic_line_t yes = from_member( jo, "yes" ); + const dynamic_line_t no = from_member( jo, "no" ); + function = [setting, yes, no]( const dialogue &d ) { + auto rule = aim_rule_strs.find( setting ); + if( rule != aim_rule_strs.end() && d.beta->rules.aim == rule->second ) { + return yes( d ); + } + return no( d ); + }; + } else if( jo.has_string( "npc_engagement_rule" ) ) { + std::string setting = jo.get_string( "npc_engagement_rule" ); + const dynamic_line_t yes = from_member( jo, "yes" ); + const dynamic_line_t no = from_member( jo, "no" ); + function = [setting, yes, no]( const dialogue &d ) { + auto rule = combat_engagement_strs.find( setting ); + if( rule != combat_engagement_strs.end() && + d.beta->rules.engagement == rule->second ) { + return yes( d ); + } + return no( d ); + }; + } else if( jo.has_string( "npc_rule" ) ) { + std::string rule = jo.get_string( "npc_rule" ); + const dynamic_line_t yes = from_member( jo, "yes" ); + const dynamic_line_t no = from_member( jo, "no" ); + function = [rule, yes, no]( const dialogue &d ) { + auto flag = ally_rule_strs.find( rule ); + if( flag != ally_rule_strs.end() && d.beta->rules.has_flag( flag->second ) ) { + return yes( d ); + } + return no( d ); + }; } else if( jo.has_bool( "has_pickup_list" ) ) { const dynamic_line_t yes = from_member( jo, "yes" ); const dynamic_line_t no = from_member( jo, "no" ); @@ -3461,20 +3260,3 @@ bool npc::item_whitelisted( const item &it ) const auto to_match = it.tname( 1, false ); return item_name_whitelisted( to_match ); } - -npc_follower_rules::npc_follower_rules() -{ - engagement = ENGAGE_CLOSE; - aim = AIM_WHEN_CONVENIENT; - use_guns = true; - use_grenades = true; - use_silent = false; - - allow_pick_up = false; - allow_bash = false; - allow_sleep = false; - allow_complain = true; - allow_pulp = true; - - close_doors = false; -} diff --git a/src/npctalk_funcs.cpp b/src/npctalk_funcs.cpp index fcbbfe511ea99..5afd44e17306c 100644 --- a/src/npctalk_funcs.cpp +++ b/src/npctalk_funcs.cpp @@ -188,7 +188,7 @@ void talk_function::stop_guard( npc &p ) void talk_function::wake_up( npc &p ) { - p.rules.allow_sleep = false; + p.rules.clear_flag( ally_rule::allow_sleep ); p.remove_effect( effect_allow_sleep ); p.remove_effect( effect_lying_down ); p.remove_effect( effect_sleep ); diff --git a/src/savegame_json.cpp b/src/savegame_json.cpp index b8c6d245cde07..2c3149e62758e 100644 --- a/src/savegame_json.cpp +++ b/src/savegame_json.cpp @@ -951,18 +951,11 @@ void npc_follower_rules::serialize( JsonOut &json ) const json.start_object(); json.member( "engagement", static_cast( engagement ) ); json.member( "aim", static_cast( aim ) ); - json.member( "use_guns", use_guns ); - json.member( "use_grenades", use_grenades ); - json.member( "use_silent", use_silent ); - - json.member( "allow_pick_up", allow_pick_up ); - json.member( "allow_bash", allow_bash ); - json.member( "allow_sleep", allow_sleep ); - json.member( "allow_complain", allow_complain ); - json.member( "allow_pulp", allow_pulp ); - - json.member( "close_doors", close_doors ); + // serialize the flags so they can be changed between save games + for( const auto &rule : ally_rule_strs ) { + json.member( rule.first, has_flag( rule.second ) ); + } json.member( "pickup_whitelist", *pickup_whitelist ); json.end_object(); @@ -977,17 +970,17 @@ void npc_follower_rules::deserialize( JsonIn &jsin ) int tmpaim = 0; data.read( "aim", tmpaim ); aim = static_cast( tmpaim ); - data.read( "use_guns", use_guns ); - data.read( "use_grenades", use_grenades ); - data.read( "use_silent", use_silent ); - - data.read( "allow_pick_up", allow_pick_up ); - data.read( "allow_bash", allow_bash ); - data.read( "allow_sleep", allow_sleep ); - data.read( "allow_complain", allow_complain ); - data.read( "allow_pulp", allow_pulp ); - data.read( "close_doors", close_doors ); + // deserialize the flags so they can be changed between save games + for( const auto &rule : ally_rule_strs ) { + bool tmpflag = false; + data.read( rule.first, tmpflag ); + if( tmpflag ) { + set_flag( rule.second ); + } else { + clear_flag( rule.second ); + } + } data.read( "pickup_whitelist", *pickup_whitelist ); } diff --git a/tests/npc_talk_test.cpp b/tests/npc_talk_test.cpp index 56403a532ee37..b9f61cfc9066c 100644 --- a/tests/npc_talk_test.cpp +++ b/tests/npc_talk_test.cpp @@ -224,6 +224,19 @@ TEST_CASE( "npc_talk_test" ) CHECK( d.responses[0].text == "This is a basic test response." ); CHECK( d.responses[1].text == "This is a npc allies 1 test response." ); + d.add_topic( "TALK_TEST_NPC_RULES" ); + gen_response_lines( d, 1 ); + CHECK( d.responses[0].text == "This is a basic test response." ); + talker_npc.rules.engagement = ENGAGE_ALL; + talker_npc.rules.aim = AIM_SPRAY; + talker_npc.rules.set_flag( ally_rule::use_silent ); + gen_response_lines( d, 4 ); + CHECK( d.responses[0].text == "This is a basic test response." ); + CHECK( d.responses[1].text == "This is a npc engagement rule test response." ); + CHECK( d.responses[2].text == "This is a npc aim rule test response." ); + CHECK( d.responses[3].text == "This is a npc rule test response." ); + talker_npc.rules.clear_flag( ally_rule::use_silent ); + d.add_topic( "TALK_TEST_NPC_NEEDS" ); gen_response_lines( d, 1 ); CHECK( d.responses[0].text == "This is a basic test response." ); @@ -451,4 +464,14 @@ TEST_CASE( "npc_talk_test" ) effects.apply( d ); CHECK( !has_item( "bottle_plastic", 1 ) ); CHECK( !has_item( "beer", 1 ) ); + + d.add_topic( "TALK_COMBAT_COMMANDS" ); + gen_response_lines( d, 7 ); + CHECK( d.responses[0].text == "Change your engagement rules..." ); + CHECK( d.responses[1].text == "Change your aiming rules..." ); + CHECK( d.responses[2].text == "Don't use ranged weapons anymore." ); + CHECK( d.responses[3].text == "Use only silent weapons." ); + CHECK( d.responses[4].text == "Don't use grenades anymore." ); + CHECK( d.responses[5].text == "Don't worry about shooting an ally." ); + CHECK( d.responses[6].text == "Never mind." ); }