Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

npctalk: add true/false responses and CONDITION trials to JSON #27707

Merged
merged 2 commits into from
Jan 20, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions data/json/npcs/TALK_TEST.json
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,28 @@
}
]
},
{
"type": "talk_topic",
"id": "TALK_TEST_TRUE_FALSE_CONDITIONAL",
"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" },
{
"truefalsetext": {
"condition": { "u_has_cash": 500 },
"true": "This is a true/false true response.",
"false": "This is a true/false false response."
},
"topic": "TALK_DONE"
},
{
"text": "This is a conditional trial response.",
"trial": { "type": "CONDITION", "condition": { "u_has_cash": 500 } },
"success": { "topic": "TALK_TEST_TRUE_CONDITION_NEXT" },
"failure": { "topic": "TALK_TEST_FALSE_CONDITION_NEXT" }
}
]
},
{
"type": "talk_topic",
"id": "TALK_TEST_EFFECTS",
Expand Down
27 changes: 21 additions & 6 deletions doc/NPCs.md
Original file line number Diff line number Diff line change
Expand Up @@ -378,14 +378,31 @@ The player will always have the option to return to a previous topic or end the
will otherwise have the option to give a $500, $50, or $5 bribe if they have the funds. If they
don't have at least $50, they will also have the option to provide some other bribe.

### truefalsetext
The player will have one response text if a condition is true, and another if it is false, but the same trial for either line. `condition`, `true`, and `false` are all mandatory.

```C++
{
"truefalsetext": {
"condition": { "u_has_cash": 800 },
"true": "I may have the money, I'm not giving you any.",
"false": "I don't have that money."
},
"topic": "TALK_WONT_PAY"
}
```

### text
Will be shown to the user, no further meaning.

### trial
Optional, if not defined, "NONE" is used. Otherwise one of "NONE", "LIE", "PERSUADE" or "INTIMIDATE". If "NONE" is used, the `failure` object is not read, otherwise it's mandatory.
The `difficulty` is only required if type is not "NONE" and specifies the success chance in percent (it is however modified by various things like mutations).
Optional, if not defined, "NONE" is used. Otherwise one of "NONE", "LIE", "PERSUADE" "INTIMIDATE", or "CONDITION". If "NONE" is used, the `failure` object is not read, otherwise it's mandatory.

The `difficulty` is only required if type is not "NONE" or "CONDITION" and specifies the success chance in percent (it is however modified by various things like mutations). Higher difficulties are easier to pass.

An optional `mod` array takes any of the following modifiers and increases the difficulty by the NPC's opinion of your character or personality trait for that modifier multiplied by the value: "ANGER", "FEAR", "TRUST", "VALUE", "AGRESSION", "ALTRUISM", "BRAVERY", "COLLECTOR". The special "POS_FEAR" modifier treats NPC's fear of your character below 0 as though it were 0. The special "TOTAL" modifier sums all previous modifiers and then multiplies the result by its value and is used when setting the owed value.

An optional `mod` array takes any of the following modifiers and increases the difficulty by the NPC's opinion of your character or personality trait for that modifier multiplied by the value: "ANGER", "FEAR", "TRUST", "VALUE", "AGRESSION", "ALTRUISM", "BRAVERY", "COLLECTOR". The special "POS_FEAR" modifier treats NPC's fear of your character below 0 as though it were 0.
"CONDITION" trials take a mandatory `condition` instead of `difficulty`. The `success` object is chosen if the `condition` is true and the `failure` is chosen otherwise.

### success and failure
Both objects have the same structure. `topic` defines which topic the dialogue will switch to. `opinion` is optional, if given it defines how the opinion of the NPC will change. The given values are *added* to the opinion of the NPC, they are all optional and default to 0. `effect` is a function that is executed after choosing the response, see below.
Expand All @@ -404,6 +421,7 @@ The `failure` object is used if the trial fails, the `success` object is used ot
### Sample trials
"trial": { "type": "PERSUADE", "difficulty": 0, "mod": [ [ "TRUST", 3 ], [ "VALUE", 3 ], [ "ANGER", -3 ] ] }
"trial": { "type": "INTIMIDATE", "difficulty": 20, "mod": [ [ "FEAR", 8 ], [ "VALUE", 2 ], [ "TRUST", 2 ], [ "BRAVERY", -2 ] ] }
"trial": { "type": "CONDITION", "condition": { "npc_has_trait": "FARMER" } }

`topic` can also be a single topic object (the `type` member is not required here):
```C++
Expand Down Expand Up @@ -651,7 +669,4 @@ Condition | Type | Description
]
}
}



```
14 changes: 13 additions & 1 deletion src/dialogue.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ enum talk_trial_type : unsigned char {
TALK_TRIAL_LIE, // Straight up lying
TALK_TRIAL_PERSUADE, // Convince them
TALK_TRIAL_INTIMIDATE, // Physical intimidation
TALK_TRIAL_CONDITION, // Some other condition
NUM_TALK_TRIALS
};

Expand All @@ -52,6 +53,7 @@ using trial_mod = std::pair<std::string, int>;
struct talk_trial {
talk_trial_type type = TALK_TRIAL_NONE;
int difficulty = 0;
std::function<bool( const dialogue & )> condition;

int calc_chance( const dialogue &d ) const;
/**
Expand Down Expand Up @@ -163,6 +165,13 @@ struct talk_response {
* displayed.
*/
std::string text;
/*
* Optional responses from a true/false test that defaults to true.
*/
std::string truetext;
std::string falsetext;
std::function<bool( const dialogue & )> truefalse_condition;

talk_trial trial;
/**
* The following values are forwarded to the chatbin of the NPC (see @ref npc_chatbin).
Expand All @@ -177,7 +186,7 @@ struct talk_response {
talk_data create_option_line( const dialogue &d, char letter );
std::set<dialogue_consequence> get_consequences( const dialogue &d ) const;

talk_response() = default;
talk_response();
talk_response( JsonObject );
};

Expand Down Expand Up @@ -297,6 +306,9 @@ struct dynamic_line_t {
}
};

// the truly awful declaration for the conditional_t loading helper_function
void read_dialogue_condition( JsonObject &jo, std::function<bool( const dialogue & )> &condition,
bool default_val );
/**
* A condition for a response spoken by the player.
* This struct only adds the constructors which will load the data from json
Expand Down
122 changes: 81 additions & 41 deletions src/npctalk.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ static std::map<std::string, json_talk_topic> json_talk_topics;

// Some aliases to help with gen_responses
#define RESPONSE(txt) ret.push_back(talk_response());\
ret.back().text = txt
ret.back().truetext = txt

#define TRIAL(tr, diff) ret.back().trial.type = tr;\
ret.back().trial.difficulty = diff
Expand Down Expand Up @@ -108,7 +108,7 @@ void bulk_trade_accept( npc &, const itype_id &it );
const std::string &talk_trial::name() const
{
static const std::array<std::string, NUM_TALK_TRIALS> texts = { {
"", _( "LIE" ), _( "PERSUADE" ), _( "INTIMIDATE" )
"", _( "LIE" ), _( "PERSUADE" ), _( "INTIMIDATE" ), ""
}
};
if( static_cast<size_t>( type ) >= texts.size() ) {
Expand Down Expand Up @@ -763,7 +763,10 @@ talk_response &dialogue::add_response( const std::string &text, const std::strin
const bool first )
{
talk_response result = talk_response();
result.text = text;
result.truetext = text;
result.truefalse_condition = []( const dialogue & ) {
return true;
};
result.success.next_topic = talk_topic( r );
if( first ) {
responses.insert( responses.begin(), result );
Expand Down Expand Up @@ -1339,7 +1342,6 @@ int talk_trial::calc_chance( const dialogue &d ) const
npc &p = *d.beta;
int chance = difficulty;
switch( type ) {
case TALK_TRIAL_NONE:
case NUM_TALK_TRIALS:
dbg( D_ERROR ) << "called calc_chance with invalid talk_trial value: " << type;
break;
Expand Down Expand Up @@ -1388,6 +1390,12 @@ int talk_trial::calc_chance( const dialogue &d ) const
chance += 20;
}
break;
case TALK_TRIAL_NONE:
chance = 100;
break;
case TALK_TRIAL_CONDITION:
chance = condition( d ) ? 100 : 0;
break;
}
for( auto this_mod: modifiers ) {
chance += parse_mod( d, this_mod.first, this_mod.second );
Expand Down Expand Up @@ -1677,18 +1685,17 @@ void dialogue::add_topic( const talk_topic &topic )
talk_data talk_response::create_option_line( const dialogue &d, const char letter )
{
std::string ftext;
if( trial != TALK_TRIAL_NONE ) { // dialogue w/ a % chance to work
ftext = string_format( pgettext( "talk option", "%1$c: [%2$s %3$d%%] %4$s" ),
letter, // option letter
trial.name(), // trial type
trial.calc_chance( d ), // trial % chance
text // response
);
} else { // regular dialogue
ftext = string_format( pgettext( "talk option", "%1$c: %2$s" ),
letter, // option letter
text // response
);
text = truefalse_condition( d ) ? truetext : falsetext;
Copy link
Member

@kevingranade kevingranade Jan 20, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spawned in a pair of NPCs, mind controlled both of them, then tried to talk to one to set it as the overseer, then it crashed on this line.
The way it crashed I can't get the backtrace.

I had also merged #27706.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, this was something I'd fixed in the final version but didn't make it into the refactoring for submission.

Fixed.

// dialogue w/ a % chance to work
if( trial.type == TALK_TRIAL_NONE || trial.type == TALK_TRIAL_CONDITION ) {
// regular dialogue
//~ %1$c is an option letter and shouldn't be translated, %2$s is translated response text
ftext = string_format( pgettext( "talk option", "%1$c: %2$s" ), letter, text );
} else {
// dialogue w/ a % chance to work
//~ %1$c is an option letter and shouldn't be translated, %2$s is translated trial type, %3$d is a number, and %4$s is the translated response text
ftext = string_format( pgettext( "talk option", "%1$c: [%2$s %3$d%%] %4$s" ), letter,
trial.name(), trial.calc_chance( d ), text );
}
parse_tags( ftext, *d.alpha, *d.beta );

Expand Down Expand Up @@ -1821,7 +1828,7 @@ talk_topic dialogue::opt( dialogue_window &d_win, const talk_topic &topic )

talk_response chosen = responses[ch];
std::string response_printed = string_format( pgettext( "you say something", "You: %s" ),
chosen.text );
response_lines[ch].second.substr( 3 ) );
d_win.add_to_history( response_printed );

if( chosen.mission_selected != nullptr ) {
Expand Down Expand Up @@ -1850,7 +1857,8 @@ talk_trial::talk_trial( JsonObject jo )
WRAP( NONE ),
WRAP( LIE ),
WRAP( PERSUADE ),
WRAP( INTIMIDATE )
WRAP( INTIMIDATE ),
WRAP( CONDITION )
#undef WRAP
}
};
Expand All @@ -1859,9 +1867,12 @@ talk_trial::talk_trial( JsonObject jo )
jo.throw_error( "invalid talk trial type", "type" );
}
type = iter->second;
if( type != TALK_TRIAL_NONE ) {
if( !( type == TALK_TRIAL_NONE || type == TALK_TRIAL_CONDITION ) ) {
difficulty = jo.get_int( "difficulty" );
}

read_dialogue_condition( jo, condition, false );

if( jo.has_array( "mod" ) ) {
JsonArray ja = jo.get_array( "mod" );
while( ja.has_more() ) {
Expand Down Expand Up @@ -2121,7 +2132,7 @@ void talk_effect_t::parse_sub_effect( JsonObject jo )
const std::string dur_string = jo.get_string( "duration" );
if( dur_string == "PERMANENT" ) {
subeffect_fun.set_u_add_permanent_effect( new_effect );
} else {
} else if( !dur_string.empty() && std::stoi( dur_string ) > 0 ) {
int duration = std::stoi( dur_string );
subeffect_fun.set_u_add_effect( new_effect, time_duration::from_turns( duration ) );
}
Expand All @@ -2135,7 +2146,7 @@ void talk_effect_t::parse_sub_effect( JsonObject jo )
const std::string dur_string = jo.get_string( "duration" );
if( dur_string == "PERMANENT" ) {
subeffect_fun.set_npc_add_permanent_effect( new_effect );
} else {
} else if( !dur_string.empty() && std::stoi( dur_string ) > 0 ) {
int duration = std::stoi( dur_string );
subeffect_fun.set_npc_add_effect( new_effect,
time_duration::from_turns( duration ) );
Expand Down Expand Up @@ -2282,6 +2293,16 @@ void talk_effect_t::load_effect( JsonObject &jo )
}
}

talk_response::talk_response()
{
truefalse_condition = []( const dialogue & ) {
return true;
};
mission_selected = nullptr;
skill = skill_id::NULL_ID();
style = matype_id::NULL_ID();
}

talk_response::talk_response( JsonObject jo )
{
if( jo.has_member( "trial" ) ) {
Expand All @@ -2302,7 +2323,17 @@ talk_response::talk_response( JsonObject jo )
if( jo.has_member( "failure" ) ) {
failure = talk_effect_t( jo.get_object( "failure" ) );
}
text = _( jo.get_string( "text" ) );
if( jo.has_member( "truefalsetext" ) ) {
JsonObject truefalse_jo = jo.get_object( "truefalsetext" );
read_dialogue_condition( truefalse_jo, truefalse_condition, true );
truetext = _( truefalse_jo.get_string( "true" ) );
falsetext = _( truefalse_jo.get_string( "false" ) );
} else {
truetext = _( jo.get_string( "text" ) );
truefalse_condition = []( const dialogue & ) {
return true;
};
}
// TODO: mission_selected
// TODO: skill
// TODO: style
Expand All @@ -2314,6 +2345,33 @@ json_talk_response::json_talk_response( JsonObject jo )
load_condition( jo );
}

void read_dialogue_condition( JsonObject &jo, std::function<bool( const dialogue & )> &condition,
bool default_val )
{
const auto null_function = [default_val]( const dialogue & ) {
return default_val;
};

static const std::string member_name( "condition" );
if( !jo.has_member( member_name ) ) {
condition = null_function;
} else if( jo.has_string( member_name ) ) {
const std::string type = jo.get_string( member_name );
conditional_t sub_condition( type );
condition = [sub_condition]( const dialogue & d ) {
return sub_condition( d );
};
} else if( jo.has_object( member_name ) ) {
const JsonObject con_obj = jo.get_object( member_name );
conditional_t sub_condition( con_obj );
condition = [sub_condition]( const dialogue & d ) {
return sub_condition( d );
};
} else {
jo.throw_error( "invalid condition syntax", member_name );
}
}

conditional_t::conditional_t( JsonObject jo )
{
const auto parse_array = []( JsonObject jo, const std::string &type ) {
Expand Down Expand Up @@ -2641,25 +2699,7 @@ void json_talk_response::load_condition( JsonObject &jo )
{
is_switch = jo.get_bool( "switch", false );
is_default = jo.get_bool( "default", false );
static const std::string member_name( "condition" );
if( !jo.has_member( member_name ) ) {
// Leave condition unset, defaults to true.
return;
} else if( jo.has_string( member_name ) ) {
const std::string type = jo.get_string( member_name );
conditional_t sub_condition( type );
condition = [sub_condition]( const dialogue & d ) {
return sub_condition( d );
};
} else if( jo.has_object( member_name ) ) {
const JsonObject con_obj = jo.get_object( member_name );
conditional_t sub_condition( con_obj );
condition = [sub_condition]( const dialogue & d ) {
return sub_condition( d );
};
} else {
jo.throw_error( "invalid condition syntax", member_name );
}
read_dialogue_condition( jo, condition, true );
}

bool json_talk_response::test_condition( const dialogue &d ) const
Expand Down
Loading