From b602ee9ae9c00b2c2de0b60abe42d352786df467 Mon Sep 17 00:00:00 2001 From: Mark Langsdorf Date: Sun, 28 Apr 2019 19:31:11 -0500 Subject: [PATCH] npctalk: add dialogue effects to give items to NPCs Create dialogue effects that let the character give items to NPCs to hold or to use, and migrate the dialogue responses that used to implement giving items to NPCs into JSON. --- data/json/npcs/TALK_COMMON_ALLY.json | 19 ++++++-- doc/NPCs.md | 13 ++++++ src/dialogue.h | 5 +- src/npctalk.cpp | 68 +++++++++++++++++----------- 4 files changed, 74 insertions(+), 31 deletions(-) diff --git a/data/json/npcs/TALK_COMMON_ALLY.json b/data/json/npcs/TALK_COMMON_ALLY.json index aeda0ada15c0a..5df225b8e7995 100644 --- a/data/json/npcs/TALK_COMMON_ALLY.json +++ b/data/json/npcs/TALK_COMMON_ALLY.json @@ -30,6 +30,10 @@ { "id": [ "TALK_FRIEND", "TALK_GIVE_ITEM", "TALK_USE_ITEM", "TALK_RADIO" ], "type": "talk_topic", + "dynamic_line": { + "is_by_radio": " *pshhhttt* I'm reading you boss, over.", + "no": { "has_reason": { "use_reason": true }, "no": "What is it, friend?" } + }, "responses": [ { "text": "Combat commands...", "topic": "TALK_COMBAT_COMMANDS" }, { "text": "Can I do anything for you?", "topic": "TALK_MISSION_LIST" }, @@ -65,8 +69,18 @@ "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", "condition": { "not": "is_by_radio" }, "topic": "TALK_USE_ITEM" }, - { "text": "Hold on to this item", "condition": { "not": "is_by_radio" }, "topic": "TALK_GIVE_ITEM" }, + { + "text": "I want you to use this item", + "condition": { "not": "is_by_radio" }, + "topic": "TALK_FRIEND", + "effect": "npc_gets_item_to_use" + }, + { + "text": "Hold on to this item", + "condition": { "not": "is_by_radio" }, + "topic": "TALK_FRIEND", + "effect": "npc_gets_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" }, @@ -531,7 +545,6 @@ { "id": "TALK_RADIO", "type": "talk_topic", - "dynamic_line": "*pshhhttt* I'm reading you boss, over.", "responses": [ { "text": "Please go to this location...", "topic": "TALK_GOTO_LOCATION_RADIO", "effect": "goto_location" }, { "text": "Stay at your current position.", "topic": "TALK_DONE", "effect": "assign_guard" }, diff --git a/doc/NPCs.md b/doc/NPCs.md index aee53aab871a4..bf84b9d8d687c 100644 --- a/doc/NPCs.md +++ b/doc/NPCs.md @@ -122,6 +122,16 @@ The dynamic line will be randomly chosen from the hints snippets. } ``` +#### Based on a previously generated reason +The dynamic line will be chosen from a reason generated by an earlier effect. The reason will be cleared. Use of it should be gated on the `"has_reason"` condition. + +```JSON +{ + "has_reason": { "use_reason": true }, + "no": "What is it, boss?" +} +``` + #### Based on any Dialogue condition The dynamic line will be chosen based on whether a single dialogue condition is true or false. Dialogue conditions cannot be chained via `"and"`, `"or"`, or `"not"`. If the condition is true, the `"yes"` response will be chosen and otherwise the `"no"` response will be chosen. Both the `'"yes"` and `"no"` reponses are optional. Simple string conditions may be followed by `"true"` to make them fields in the dynamic line dictionary, or they can be followed by the response that will be chosen if the condition is true and the `"yes"` response can be omitted. @@ -400,6 +410,8 @@ start_trade | Opens the trade screen and allows trading with the NPC. buy_10_logs | Places 10 logs in the ranch garage, and makes the NPC unavailable for 1 day. buy_100_logs | Places 100 logs in the ranch garage, and makes the NPC unavailable for 7 days. give_equipment | Allows your character to select items from the NPC's inventory and transfer them to your inventory. +npc_gets_item | Allows your character to select an item from your character's inventory and transfer it to the NPC's inventory. The NPC will not accept it if they do not have space or weight to carry it, and will set a reason that can be referenced in a future dynamic line with `"use_reason"`. +npc_gets_item_to_use | Allow your character to select an item from your character's inventory and transfer it to the NPC's inventory. The NPC will attempt to wield it and will not accept it if it too heavy or is an inferior weapon to what they are currently using, and will set a reason that can be referenced in a future dynamic line with `"use_reason"`. u_buy_item: item_string, (*optional* cost: cost_num, *optional* count: count_num, *optional* container: container_string) | The NPC will give your character the item or `count_num` copies of the item, contained in container, and will remove `cost_num` from your character's cash if specified.
If cost isn't present, the NPC gives your character the item at no charge. u_sell_item: item_string, (*optional* cost: cost_num, *optional* count: count_num) | Your character will give the NPC the item or `count_num` copies of the item, and will add `cost_num` to your character's cash if specified.
If cost isn't present, the your character gives the NPC the item at no charge.
This effect will fail if you do not have at least `count_num` copies of the item, so it should be checked with `u_has_items`. u_bulk_trade_accept
npc_bulk_trade_accept | Only valid after a repeat_response. The player trades all instances of the item from the repeat_response with the NPC. For u_bulk_trade_accept, the player loses the items from their inventory and gains cash; for npc_bulk_trade_accept, the player gains the items from the NPC's inventory and loses cash. @@ -556,6 +568,7 @@ Condition | Type | Description "npc_train_styles" | simple string | `true` if the NPC knows one or more martial arts styles that the player does not know. "npc_has_class" | array | `true` if the NPC is a member of an NPC class. "npc_role_nearby" | string | `true` if there is an NPC with the same companion mission role as `npc_role_nearby` within 100 tiles. +"has_reason" | simple_string" | `true` if a previous effect set a reason for why an effect could not be completed. #### NPC Follower AI rules Condition | Type | Description diff --git a/src/dialogue.h b/src/dialogue.h index 12b820eaa77f3..0efc579360e7a 100644 --- a/src/dialogue.h +++ b/src/dialogue.h @@ -117,6 +117,7 @@ struct talk_effect_fun_t { void set_npc_aim_rule( const std::string &setting ); void set_mapgen_update( JsonObject jo, const std::string &member ); void set_bulk_trade_accept( bool is_trade, bool is_npc = false ); + void set_npc_gets_item( bool to_use ); void operator()( const dialogue &d ) const { if( !function ) { @@ -234,6 +235,7 @@ struct dialogue { dialogue() = default; mutable itype_id cur_item; + mutable std::string reason; std::string dynamic_line( const talk_topic &topic ) const; void apply_speaker_effects( const talk_topic &the_topic ); @@ -342,7 +344,7 @@ const std::unordered_set simple_string_conds = { { "at_safe_space", "is_day", "is_outside", "u_has_camp", "u_can_stow_weapon", "npc_can_stow_weapon", "u_has_weapon", "npc_has_weapon", "u_driving", "npc_driving", - "has_pickup_list", "is_by_radio", + "has_pickup_list", "is_by_radio", "has_reason" } }; const std::unordered_set complex_conds = { { @@ -432,6 +434,7 @@ struct conditional_t { void set_is_by_radio(); void set_u_has_camp(); void set_has_pickup_list(); + void set_has_reason(); void set_is_gender( bool is_male, bool is_npc = false ); bool operator()( const dialogue &d ) const { diff --git a/src/npctalk.cpp b/src/npctalk.cpp index 6b7b9a2ec44c7..34855a545adb0 100644 --- a/src/npctalk.cpp +++ b/src/npctalk.cpp @@ -639,8 +639,6 @@ std::string dialogue::dynamic_line( const talk_topic &the_topic ) const response = string_format( ngettext( "%d foot.", "%d feet.", dist ), dist ); } return response; - } else if( topic == "TALK_FRIEND" ) { - return _( "What is it?" ); } else if( topic == "TALK_DESCRIBE_MISSION" ) { switch( p->mission ) { case NPC_MISSION_SHELTER: @@ -741,11 +739,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_USE_ITEM" ) { - return give_item_to( *p, true, false ); - } else if( topic == "TALK_GIVE_ITEM" ) { - return give_item_to( *p, false, true ); - // Maybe TODO: Allow an option to "just take it, use it if you want" } else if( topic == "TALK_MIND_CONTROL" ) { bool not_following = g->get_follower_list().count( p->getID() ) == 0; p->companion_mission_role_id.clear(); @@ -1823,6 +1816,13 @@ void talk_effect_fun_t::set_bulk_trade_accept( bool is_trade, bool is_npc ) }; } +void talk_effect_fun_t::set_npc_gets_item( bool to_use ) +{ + function = [to_use]( const dialogue & d ) { + d.reason = give_item_to( *( d.beta ), to_use, !to_use ); + }; +} + void talk_effect_t::set_effect_consequence( const talk_effect_fun_t &fun, dialogue_consequence con ) { effects.push_back( fun ); @@ -2083,9 +2083,9 @@ void talk_effect_t::parse_string_effect( const std::string &effect_id, JsonObjec return; } + talk_effect_fun_t subeffect_fun; if( effect_id == "u_bulk_trade_accept" || effect_id == "npc_bulk_trade_accept" || effect_id == "u_bulk_donate" || effect_id == "npc_bulk_donate" ) { - talk_effect_fun_t subeffect_fun; bool is_npc = effect_id == "npc_bulk_trade_accept" || effect_id == "npc_bulk_donate"; bool is_trade = effect_id == "u_bulk_trade_accept" || effect_id == "npc_bulk_trade_accept"; subeffect_fun.set_bulk_trade_accept( is_trade, is_npc ); @@ -2093,6 +2093,13 @@ void talk_effect_t::parse_string_effect( const std::string &effect_id, JsonObjec return; } + if( effect_id == "npc_gets_item" || effect_id == "npc_gets_item_to_use" ) { + bool to_use = effect_id == "npc_gets_item_to_use"; + subeffect_fun.set_npc_gets_item( to_use ); + set_effect( subeffect_fun ); + return; + } + jo.throw_error( "unknown effect string", effect_id ); } @@ -2836,6 +2843,13 @@ void conditional_t::set_is_by_radio() }; } +void conditional_t::set_has_reason() +{ + condition = []( const dialogue & d ) { + return !d.reason.empty(); + }; +} + conditional_t::conditional_t( JsonObject jo ) { // improve the clarity of NPC setter functions @@ -3070,6 +3084,8 @@ conditional_t::conditional_t( const std::string &type ) set_has_pickup_list(); } else if( type == "is_by_radio" ) { set_is_by_radio(); + } else if( type == "has_reason" ) { + set_has_reason(); } else { condition = []( const dialogue & ) { return false; @@ -3166,6 +3182,12 @@ dynamic_line_t::dynamic_line_t( JsonObject jo ) function = [&]( const dialogue & ) { return get_hint(); }; + } else if( jo.has_member( "use_reason" ) ) { + function = [&]( const dialogue & d ) { + std::string tmp = d.reason; + d.reason.clear(); + return tmp; + }; } else { conditional_t dcondition; const dynamic_line_t yes = from_member( jo, "yes" ); @@ -3534,44 +3556,36 @@ std::string give_item_to( npc &p, bool allow_use, bool allow_carry ) return _( "Thanks!" ); } - std::stringstream reason; - reason << _( "Nope." ); - reason << std::endl; + std::string reason = _( "Nope." ); if( allow_use ) { if( !no_consume_reason.empty() ) { - reason << no_consume_reason; - reason << std::endl; + reason += no_consume_reason + "\n"; } - reason << _( "My current weapon is better than this." ); - reason << std::endl; - reason << string_format( _( "(new weapon value: %.1f vs %.1f)." ), - new_weapon_value, cur_weapon_value ); + reason += _( "My current weapon is better than this." ); + reason += "\n" + string_format( _( "(new weapon value: %.1f vs %.1f)." ), new_weapon_value, + cur_weapon_value ); if( !given.is_gun() && given.is_armor() ) { - reason << std::endl; - reason << string_format( _( "It's too encumbering to wear." ) ); + reason += "\n" + string_format( _( "It's too encumbering to wear." ) ); } } if( allow_carry ) { if( !p.can_pickVolume( given ) ) { const units::volume free_space = p.volume_capacity() - p.volume_carried(); - reason << std::endl; - reason << string_format( _( "I have no space to store it." ) ); - reason << std::endl; + reason += "\n" + string_format( _( "I have no space to store it." ) ) + "\n"; if( free_space > 0_ml ) { - reason << string_format( _( "I can only store %s %s more." ), + reason += string_format( _( "I can only store %s %s more." ), format_volume( free_space ), volume_units_long() ); } else { - reason << string_format( _( "...or to store anything else for that matter." ) ); + reason += string_format( _( "...or to store anything else for that matter." ) ); } } if( !p.can_pickWeight( given ) ) { - reason << std::endl; - reason << string_format( _( "It is too heavy for me to carry." ) ); + reason += "\n" + string_format( _( "It is too heavy for me to carry." ) ); } } - return reason.str(); + return reason; } bool npc::has_item_whitelist() const