Skip to content

Commit

Permalink
Merge pull request #27707 from mlangsdorf/npctalk_tf_conditions
Browse files Browse the repository at this point in the history
npctalk: add true/false responses and CONDITION trials to JSON
  • Loading branch information
kevingranade authored Jan 20, 2019
2 parents ae1e5be + e9566ac commit 9e9d787
Show file tree
Hide file tree
Showing 5 changed files with 219 additions and 138 deletions.
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;
// 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

0 comments on commit 9e9d787

Please sign in to comment.