Skip to content

Commit

Permalink
More generic compares and arithmetic for the dialogue system (#50305)
Browse files Browse the repository at this point in the history
  • Loading branch information
Light-Wave authored Aug 21, 2021
1 parent 804fa93 commit 33eec6b
Show file tree
Hide file tree
Showing 10 changed files with 1,894 additions and 5 deletions.
593 changes: 593 additions & 0 deletions data/json/npcs/TALK_TEST.json

Large diffs are not rendered by default.

52 changes: 52 additions & 0 deletions doc/NPCs.md
Original file line number Diff line number Diff line change
Expand Up @@ -765,6 +765,58 @@ Condition | Type | Description
`"is_pressure"` | int or variable_object | `true` if current pressure is at least `"is_pressure"`( or the value of the variable described see `variable_object` above).
`"is_weather"` | int or variable_object | `true` if current weather is `"is_weather"`.


#### Compare itergers & arithmetics
`"compare_int"` can be used to compare two values to each other, while `"arithmetic"` can be used to take up to two values, perform arithmetic on them, and then save them in a third value. The syntax is as follows.
```
{
"text": "If player strength is more than or equal to 5, sets time since cataclysm to the player's focus times the player's maximum mana.",
"topic": "TALK_DONE",
"condition": { "compare_int": [ { "u_val": "strength" }, { "const": 5 } ], "op": ">=" }
"effect": { "arithmetic": [ { "time_since_cataclysm": "turns" }, { "u_val": "focus" }, { "u_val": "mana_max" } ], "op": "*" }
},
```

`"compare_int"` supports the following opperators: `"=="`, `"="` (Both are treated the same, as a compare), `"!="`, `"<="`, `">="`, `"<"`, and `">"`.

`"arithmetic"` supports the following opperators: `"*"`, `"/"`, `"+"`, `"-"`, `"%"`, `"&"`, `"|"`, `"<<"`, `">>"`, `"~"`, `"^"`, `"="`, `"*="`, `"/="`, `"+="`, `"-="`, `"%="`, `"++"`, and `"--"`

To get player character properties, use `"u_val"`. To get NPC properties, use same syntax but `"npc_val"` instead. A list of values that can be read and/or witen to follows.

Example | Description
--- | ---
`"const": 5` | A constant value, in this case 5. Can be read but not written to.
`"time": "5 days"` | A constant time value. Will be converted to turns. Can be read but not written to.
`"time_since_cataclysm": "turns"` | Time since the start of the cataclysm in turns. Can instead take other time units such as minutes, hours, days, weeks, seasons, and years.
`"rand": 20` | A random value between 0 and a given value, in this case 20. Can be read but not written to.
`"weather": "temperature"` | Current temperature.
`"weather": "windpower"` | Current windpower.
`"weather": "humidity"` | Current humidity.
`"weather": "pressure"` | Current pressure.
`"u_val": "strength"` | Player character's strength. Can be read but not written to. Replace `"strength"` with `"dexterity"`, `"intelligence"`, or `"perception"` to get such values.
`"u_val": "strength_base"` | Player character's strength. Replace `"strength_base"` with `"dexterity_base"`, `"intelligence_base"`, or `"perception_base"` to get such values.
`"u_val": "var"` | Custom variable. `"var_name"`, `"type"`, and `"context"` must also be specified.
`"u_val": "time_since_var"` | Time since a custom variable was set. Unit used it turns. `"var_name"`, `"type"`, and `"context"` must also be specified.
`"u_val": "allies"` | Number of allies the character has. Only supported for the player character. Can be read but not written to.
`"u_val": "cash"` | Ammount of money the character has. Only supported for the player character. Can be read but not written to.
`"u_val": "owed"` | Owed money to the NPC you're talking to.
`"u_val": "skill_level"` | Level in given skill. `"skill"` must also be specified.
`"u_val": "pos_x"` | Player character x coordinate. "pos_y" and "pos_z" also works as expected.
`"u_val": "pain"` | Pain level.
`"u_val": "power"` | Bionic power in milijoule.
`"u_val": "power_max"` | Max bionic power in milijoule. Can be read but not written to.
`"u_val": "power_percentage"` | Percentage of max bionic power. Should be a number between 0 to 100.
`"u_val": "morale"` | The current morale. Can be read but not written to.
`"u_val": "mana"` | Current mana.
`"u_val": "mana_max"` | Max mana. Can be read but not written to.
`"u_val": "hunger"` | Current perceived hunger. Can be read but not written to.
`"u_val": "thirst"` | Current thirst.
`"u_val": "stored_kcal"` | Stored kcal in the character's body. 55'000 is considered healthy.
`"u_val": "stored_kcal_percentage"` | a value of 100 represents 55'000 kcal, which is considered healthy.
`"u_val": "item_count"` | Number of a given item in the character's inventory. `"item"` must also be specified. Can be read but not written to.
`"u_val": "exp"` | Total experience earned. Not supported for NPCs. Can be read but not written to.


#### Sample responses with conditions and effects
```json
{
Expand Down
292 changes: 292 additions & 0 deletions src/condition.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#include "item.h"
#include "item_category.h"
#include "json.h"
#include "kill_tracker.h"
#include "line.h"
#include "map.h"
#include "mapdata.h"
Expand Down Expand Up @@ -873,6 +874,295 @@ void conditional_t<T>::set_is_weather( const JsonObject &jo )
return get_weather().weather_id == weather;
};
}

template<class T>
void conditional_t<T>::set_compare_int( const JsonObject &jo, const std::string &member )
{
JsonArray objects = jo.get_array( member );
if( objects.size() != 2 ) {
jo.throw_error( "incorrect number of values. Expected two in " + jo.str() );
condition = []( const T & ) {
return false;
};
return;
}
std::function<int( const T & )> get_first_int = get_get_int( objects.get_object( 0 ) );
std::function<int( const T & )> get_second_int = get_get_int( objects.get_object( 1 ) );
const std::string &op = jo.get_string( "op" );

if( op == "==" || op == "=" ) {
condition = [get_first_int, get_second_int]( const T & d ) {
return get_first_int( d ) == get_second_int( d );
};
} else if( op == "!=" ) {
condition = [get_first_int, get_second_int]( const T & d ) {
return get_first_int( d ) != get_second_int( d );
};
} else if( op == "<=" ) {
condition = [get_first_int, get_second_int]( const T & d ) {
return get_first_int( d ) <= get_second_int( d );
};
} else if( op == ">=" ) {
condition = [get_first_int, get_second_int]( const T & d ) {
return get_first_int( d ) >= get_second_int( d );
};
} else if( op == "<" ) {
condition = [get_first_int, get_second_int]( const T & d ) {
return get_first_int( d ) < get_second_int( d );
};
} else if( op == ">" ) {
condition = [get_first_int, get_second_int]( const T & d ) {
return get_first_int( d ) > get_second_int( d );
};
} else {
jo.throw_error( "unexpected operator " + jo.get_string( "op" ) + " in " + jo.str() );
condition = []( const T & ) {
return false;
};
}
}

template<class T>
std::function<int( const T & )> conditional_t<T>::get_get_int( const JsonObject &jo )
{
if( jo.has_member( "const" ) ) {
const int const_value = jo.get_int( "const" );
return [const_value]( const T & ) {
return const_value;
};
} else if( jo.has_member( "time" ) ) {
const int value = to_turns<int>( read_from_json_string<time_duration>( *jo.get_raw( "time" ),
time_duration::units ) );
return [value]( const T & ) {
return value;
};
} else if( jo.has_member( "time_since_cataclysm" ) ) {
time_duration given_unit = 1_turns;
if( jo.has_string( "time_since_cataclysm" ) ) {
std::string given_unit_str = jo.get_string( "time_since_cataclysm" );
bool found = false;
for( const auto &pair : time_duration::units ) {
const std::string &unit = pair.first;
if( unit == given_unit_str ) {
given_unit = pair.second;
found = true;
break;
}
}
if( !found ) {
jo.throw_error( "unrecognized time unit in " + jo.str() );
}
}
return [given_unit]( const T & ) {
return to_turn<int>( calendar::turn ) / to_turns<int>( given_unit );
};
} else if( jo.has_member( "rand" ) ) {
int max_value = jo.get_int( "rand" );
return [max_value]( const T & ) {
return rng( 0, max_value );
};
} else if( jo.has_member( "weather" ) ) {
std::string weather_aspect = jo.get_string( "weather" );
if( weather_aspect == "temperature" ) {
return []( const T & ) {
return get_weather().weather_precise->temperature;
};
} else if( weather_aspect == "windpower" ) {
return []( const T & ) {
return get_weather().weather_precise->windpower;
};
} else if( weather_aspect == "humidity" ) {
return []( const T & ) {
return get_weather().weather_precise->humidity;
};
} else if( weather_aspect == "pressure" ) {
return []( const T & ) {
return get_weather().weather_precise->pressure;
};
}
} else if( jo.has_member( "u_val" ) || jo.has_member( "npc_val" ) ) {
const bool is_npc = jo.has_member( "npc_val" );
const std::string checked_value = is_npc ? jo.get_string( "npc_val" ) : jo.get_string( "u_val" );
if( checked_value == "strength" ) {
return [is_npc]( const T & d ) {
return d.actor( is_npc )->str_cur();
};
} else if( checked_value == "dexterity" ) {
return [is_npc]( const T & d ) {
return d.actor( is_npc )->dex_cur();
};
} else if( checked_value == "intelligence" ) {
return [is_npc]( const T & d ) {
return d.actor( is_npc )->int_cur();
};
} else if( checked_value == "perception" ) {
return [is_npc]( const T & d ) {
return d.actor( is_npc )->per_cur();
};
} else if( checked_value == "strength_base" ) {
return [is_npc]( const T & d ) {
return d.actor( is_npc )->get_str_max();
};
} else if( checked_value == "dexterity_base" ) {
return [is_npc]( const T & d ) {
return d.actor( is_npc )->get_dex_max();
};
} else if( checked_value == "intelligence_base" ) {
return [is_npc]( const T & d ) {
return d.actor( is_npc )->get_int_max();
};
} else if( checked_value == "perception_base" ) {
return [is_npc]( const T & d ) {
return d.actor( is_npc )->get_per_max();
};
} else if( checked_value == "var" ) {
const std::string var_name = get_talk_varname( jo, "var_name", false );
return [is_npc, var_name]( const T & d ) {
int stored_value = 0;
const std::string &var = d.actor( is_npc )->get_value( var_name );
if( !var.empty() ) {
stored_value = std::stoi( var );
}
return stored_value;
};
} else if( checked_value == "time_since_var" ) {
const std::string var_name = get_talk_varname( jo, "var_name", false );
return [is_npc, var_name]( const T & d ) {
int stored_value = 0;
const std::string &var = d.actor( is_npc )->get_value( var_name );
if( !var.empty() ) {
stored_value = std::stoi( var );
}
return to_turn<int>( calendar::turn ) - stored_value;
};
} else if( checked_value == "allies" ) {
if( is_npc ) {
jo.throw_error( "allies count not supported for NPCs. In " + jo.str() );
} else {
return []( const T & ) {
return g->allies().size();
};
}
} else if( checked_value == "cash" ) {
if( is_npc ) {
jo.throw_error( "cash count not supported for NPCs. In " + jo.str() );
} else {
return [is_npc]( const T & d ) {
return d.actor( is_npc )->cash();
};
}
} else if( checked_value == "owed" ) {
if( is_npc ) {
jo.throw_error( "owed ammount not supported for NPCs. In " + jo.str() );
} else {
return []( const T & d ) {
return d.actor( true )->debt();
};
}
} else if( checked_value == "skill_level" ) {
const skill_id skill( jo.get_string( "skill" ) );
return [is_npc, skill]( const T & d ) {
return d.actor( is_npc )->get_skill_level( skill );
};
} else if( checked_value == "pos_x" ) {
return [is_npc]( const T & d ) {
return d.actor( is_npc )->posx();
};
} else if( checked_value == "pos_y" ) {
return [is_npc]( const T & d ) {
return d.actor( is_npc )->posy();
};
} else if( checked_value == "pos_z" ) {
return [is_npc]( const T & d ) {
return d.actor( is_npc )->posz();
};
} else if( checked_value == "pain" ) {
return [is_npc]( const T & d ) {
return d.actor( is_npc )->pain_cur();
};
} else if( checked_value == "power" ) {
return [is_npc]( const T & d ) {
// Energy in milijoule
return d.actor( is_npc )->power_cur().value();
};
} else if( checked_value == "power_max" ) {
return [is_npc]( const T & d ) {
// Energy in milijoule
return d.actor( is_npc )->power_max().value();
};
} else if( checked_value == "power_percentage" ) {
return [is_npc]( const T & d ) {
// Energy in milijoule
int power_max = d.actor( is_npc )->power_max().value();
if( power_max == 0 ) {
return 0; //Default value if character does not have power, avoids division with 0.
} else {
return ( d.actor( is_npc )->power_cur().value() * 100 ) / power_max;
}
};
} else if( checked_value == "morale" ) {
return [is_npc]( const T & d ) {
return d.actor( is_npc )->morale_cur();
};
} else if( checked_value == "focus" ) {
return [is_npc]( const T & d ) {
return d.actor( is_npc )->focus_cur();
};
} else if( checked_value == "mana" ) {
return [is_npc]( const T & d ) {
return d.actor( is_npc )->mana_cur();
};
} else if( checked_value == "mana_max" ) {
return [is_npc]( const T & d ) {
return d.actor( is_npc )->mana_max();
};
} else if( checked_value == "mana_percentage" ) {
return [is_npc]( const T & d ) {
int mana_max = d.actor( is_npc )->mana_max();
if( mana_max == 0 ) {
return 0; //Default value if character does not have mana, avoids division with 0.
} else {
return ( d.actor( is_npc )->mana_cur() * 100 ) / mana_max;
}
};
} else if( checked_value == "hunger" ) {
return [is_npc]( const T & d ) {
return d.actor( is_npc )->get_hunger();
};
} else if( checked_value == "thirst" ) {
return [is_npc]( const T & d ) {
return d.actor( is_npc )->get_thirst();
};
} else if( checked_value == "stored_kcal" ) {
return [is_npc]( const T & d ) {
return d.actor( is_npc )->get_stored_kcal();
};
} else if( checked_value == "stored_kcal_percentage" ) {
// 100% is 55'000 kcal, which is considered healthy.
return [is_npc]( const T & d ) {
return d.actor( is_npc )->get_stored_kcal() / 550;
};
} else if( checked_value == "item_count" ) {
const itype_id item_id( jo.get_string( "item" ) );
return [is_npc, item_id]( const T & d ) {
return std::max( d.actor( is_npc )->charges_of( item_id ),
d.actor( is_npc )->get_amount( item_id ) );
};
} else if( checked_value == "exp" ) {
if( is_npc ) {
jo.throw_error( "exp not currently supported for npcs. In " + jo.str() );
}
return []( const T & ) {
return g->get_kill_tracker().kill_xp();
};
}
}
jo.throw_error( "unrecognized interger sournce in " + jo.str() );
return []( const T & ) {
return 0;
};
}

template<class T>
void conditional_t<T>::set_u_has_camp()
{
Expand Down Expand Up @@ -1260,6 +1550,8 @@ conditional_t<T>::conditional_t( const JsonObject &jo )
set_is_in_field( jo, "npc_is_in_field", is_npc );
} else if( jo.has_string( "is_weather" ) ) {
set_is_weather( jo );
} else if( jo.has_member( "compare_int" ) ) {
set_compare_int( jo, "compare_int" );
} else {
for( const std::string &sub_member : dialogue_data::simple_string_conds ) {
if( jo.has_string( sub_member ) ) {
Expand Down
5 changes: 4 additions & 1 deletion src/condition.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ const std::unordered_set<std::string> complex_conds = { {
"is_temperature", "is_windpower", "is_humidity", "is_pressure", "u_is_height", "npc_is_height",
"u_has_worn_with_flag", "npc_has_worn_with_flag", "u_has_wielded_with_flag", "npc_has_wielded_with_flag",
"u_has_pain", "npc_has_pain", "u_has_power", "npc_has_power", "u_has_focus", "npc_has_focus", "u_has_morale",
"npc_has_morale", "u_is_on_terrain", "npc_is_on_terrain", "u_is_in_field", "npc_is_in_field"
"npc_has_morale", "u_is_on_terrain", "npc_is_on_terrain", "u_is_in_field", "npc_is_in_field", "compare_int"
}
};
} // namespace dialogue_data
Expand Down Expand Up @@ -183,6 +183,9 @@ struct conditional_t {
void set_can_see( bool is_npc = false );
void set_has_morale( const JsonObject &jo, const std::string &member, bool is_npc = false );
void set_has_focus( const JsonObject &jo, const std::string &member, bool is_npc = false );
void set_compare_int( const JsonObject &jo, const std::string &member );
static std::function<int( const T & )> get_get_int( const JsonObject &jo );

bool operator()( const T &d ) const {
if( !condition ) {
return false;
Expand Down
2 changes: 2 additions & 0 deletions src/dialogue.h
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ struct talk_effect_fun_t {
void set_mod_focus( const JsonObject &jo, const std::string &member, bool is_npc );
void set_add_morale( const JsonObject &jo, const std::string &member, bool is_npc );
void set_lose_morale( const JsonObject &jo, const std::string &member, bool is_npc );
void set_arithmetic( const JsonObject &jo, const std::string &member );
std::function<void( const dialogue &, int )> get_set_int( const JsonObject &jo );
void set_custom_light_level( const JsonObject &jo, const std::string &member );
void set_spawn_monster( const JsonObject &jo, const std::string &member, bool is_npc );
void set_mod_radiation( const JsonObject &jo, const std::string &member, bool is_npc );
Expand Down
Loading

0 comments on commit 33eec6b

Please sign in to comment.