Skip to content

Commit

Permalink
npctalk: add dialogue effects to give items to NPCs
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
mlangsdorf committed Apr 30, 2019
1 parent c0cef43 commit 5d84d3a
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 34 deletions.
23 changes: 18 additions & 5 deletions data/json/npcs/TALK_COMMON_ALLY.json
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand All @@ -53,20 +57,30 @@
"failure": { "topic": "TALK_TRAIN_PERSUADE" }
},
{
"text": "Let's trade items",
"text": "Let's trade items.",
"condition": { "not": "is_by_radio" },
"topic": "TALK_FRIEND",
"effect": "start_trade"
},
{
"text": "Guard this position",
"text": "Guard this position.",
"condition": { "not": "is_by_radio" },
"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", "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" },
Expand Down Expand Up @@ -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" },
Expand Down
13 changes: 13 additions & 0 deletions doc/NPCs.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,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.

Expand Down Expand Up @@ -410,6 +420,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.<br/>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.<br/>If cost isn't present, the your character gives the NPC the item at no charge.<br/>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<br/>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.
Expand Down Expand Up @@ -566,6 +578,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
Expand Down
5 changes: 4 additions & 1 deletion src/dialogue.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 ) {
Expand Down Expand Up @@ -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 );
Expand Down Expand Up @@ -342,7 +344,7 @@ const std::unordered_set<std::string> 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<std::string> complex_conds = { {
Expand Down Expand Up @@ -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 {
Expand Down
68 changes: 41 additions & 27 deletions src/npctalk.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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 );
Expand Down Expand Up @@ -2083,16 +2083,23 @@ 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 );
set_effect( subeffect_fun );
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 );
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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" );
Expand Down Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion tools/dialogue_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
import os

# no one references these, but we may want them again some day
OBSOLETE_TOPICS = [ "TALK_DENY_GUARD", "TALK_FRIEND_UNCOMFORTABLE" ]
OBSOLETE_TOPICS = [
"TALK_DENY_GUARD", "TALK_FRIEND_UNCOMFORTABLE", "TALK_USE_ITEM", "TALK_GIVE_ITEM"
]


args = argparse.ArgumentParser(description="Confirm that every talk topic in every response in a "
Expand Down

0 comments on commit 5d84d3a

Please sign in to comment.