Skip to content

Commit

Permalink
factions: allow defining custom price rules
Browse files Browse the repository at this point in the history
  • Loading branch information
andrei8l committed Jun 4, 2022
1 parent 19ac50e commit 766497b
Show file tree
Hide file tree
Showing 11 changed files with 178 additions and 10 deletions.
1 change: 1 addition & 0 deletions data/json/npcs/factions.json
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@
"food_supply": 115200,
"wealth": 75000000,
"currency": "FMCNote",
"price_rules": [ { "itype": "money_strap_FMCNote", "fixed_adj": 0 }, { "itype": "money_bundle_FMCNote", "fixed_adj": 0 } ],
"relations": {
"free_merchants": {
"kill on sight": false,
Expand Down
25 changes: 25 additions & 0 deletions data/mods/TEST_DATA/factions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[
{
"type": "faction",
"id": "debuggers",
"name": "Brave Debuggers",
"likes_u": 1000,
"respects_u": 1000,
"known_by_u": true,
"size": 5,
"power": 9001,
"food_supply": 1,
"wealth": 0,
"currency": "FMCNote",
"price_rules": [
{ "itype": "test_pants_fur", "fixed_adj": 0 },
{
"itype": "test_nuclear_carafe",
"markup": 2.0,
"fixed_adj": 0.1,
"condition": { "npc_has_var": "thirsty", "type": "bool", "context": "allnighter", "value": "yes" }
}
],
"description": "They debug so you don't have to."
}
]
3 changes: 2 additions & 1 deletion data/mods/TEST_DATA/npc_shop_cons_rates.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@
"type": "npc",
"id": "test_npc_trader",
"class": "test_npc_trader_class",
"faction": "debuggers",
"attitude": 1,
"mission": 0,
"mission": 3,
"chat": "TALK_TEST_START"
}
]
11 changes: 11 additions & 0 deletions doc/FACTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@ An NPC faction looks like this:
"lone_wolf_faction": true,
"wealth": 75000000,
"currency": "FMCNote",
"price_rules": [
{ "itype": "money_strap_FMCNote", "fixed_adj": 0 },
{ "category": "tools", "markup": 1.5 },
{
"group": "test_item_group",
"markup": 2.0,
"fixed_adj": 0.1,
"condition": { "npc_has_var": "thirsty", "type": "bool", "context": "allnighter", "value": "yes" }
}
],
"relations": {
"free_merchants": {
"kill on sight": false,
Expand Down Expand Up @@ -58,6 +68,7 @@ Field | Meaning
`"food_supply"` | integer, the number of calories available to the faction. Has no effect in play currently.
`"wealth"` | integer, number of post-apocalyptic currency in cents that that faction has to purchase stuff.
`"currency"` | string, the item `"id"` of the faction's preferred currency. Faction shopkeeps will trade faction current at 100% value, for both selling and buying.
`"price_rules"` | array, allows defining `markup` and/or `fixed_adj` for an `itype`/`category`/`group`. `markup` is only used when an NPC is selling to the avatar and defaults to `1`. `fixed_adj` is used instead of adjustment based on social skill and intelligence stat and can be used to define secondary currencies. Lower entries override higher ones. For conditionals, the avatar is used as alpha and the evaluating npc as beta
`"relations"` | dictionary, a description of how the faction sees other factions. See below
`"mon_faction"` | string, optional. The monster faction `"name"` of the monster faction that this faction counts as. Defaults to "human" if unspecified.
`"lone_wolf_faction"` | bool, optional. This is a proto/micro faction template that is used to generate 1-person factions for dynamically spawned NPCs, defaults to "false" if unspecified.
Expand Down
34 changes: 34 additions & 0 deletions src/faction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#include "catacharset.h"
#include "character.h"
#include "coordinates.h"
#include "condition.h"
#include "cursesdef.h"
#include "debug.h"
#include "display.h"
Expand All @@ -24,6 +25,7 @@
#include "game_constants.h"
#include "input.h"
#include "item.h"
#include "item_group.h"
#include "json.h"
#include "line.h"
#include "localized_comparator.h"
Expand Down Expand Up @@ -108,6 +110,18 @@ void faction_template::load_relations( const JsonObject &jsobj )
}
}

void faction_price_rules::deserialize( JsonObject const &jo )
{
optional( jo, false, "itype", itype );
optional( jo, false, "category", category );
optional( jo, false, "group", item_group );
optional( jo, false, "markup", markup, 1.0 );
optional( jo, false, "fixed_adj", fixed_adj, cata::nullopt );
if( jo.has_member( "condition" ) ) {
read_condition<dialogue>( jo, "condition", condition, false );
}
}

faction_template::faction_template( const JsonObject &jsobj )
: name( jsobj.get_string( "name" ) )
, likes_u( jsobj.get_int( "likes_u" ) )
Expand All @@ -121,8 +135,10 @@ faction_template::faction_template( const JsonObject &jsobj )
, wealth( jsobj.get_int( "wealth" ) )
{
jsobj.get_member( "description" ).read( desc );
optional( jsobj, false, "price_rules", price_rules );
if( jsobj.has_string( "currency" ) ) {
jsobj.read( "currency", currency, true );
price_rules.emplace_back( currency, 1, 0 );
} else {
currency = itype_id::NULL_ID();
}
Expand Down Expand Up @@ -341,6 +357,23 @@ nc_color faction::food_supply_color()
}
}

faction_price_rules const *faction::get_price_rules( item const &it, npc const &guy ) const
{
dialogue const temp( get_talker_for( get_avatar() ), get_talker_for( guy ) );
auto const el = std::find_if(
price_rules.crbegin(), price_rules.crend(), [&it, &temp]( faction_price_rules const & fc ) {
return ( !fc.condition or fc.condition( temp ) ) and
( fc.itype.is_empty() or it.typeId() == fc.itype ) and
( fc.category.is_empty() or it.get_category_shallow().id == fc.category ) and
( fc.item_group.is_empty() or
item_group::group_contains_item( fc.item_group, it.typeId() ) );
} );
if( el != price_rules.crend() ) {
return &*el;
}
return nullptr;
}

bool faction::has_relationship( const faction_id &guy_id, npc_factions::relationship flag ) const
{
for( const auto &rel_data : relations ) {
Expand Down Expand Up @@ -440,6 +473,7 @@ faction *faction_manager::get( const faction_id &id, const bool complain )
for( const faction_template &fac_temp : npc_factions::all_templates ) {
if( fac_temp.id == id ) {
elem.second.currency = fac_temp.currency;
elem.second.price_rules = fac_temp.price_rules;
elem.second.lone_wolf_faction = fac_temp.lone_wolf_faction;
elem.second.name = fac_temp.name;
elem.second.desc = fac_temp.desc;
Expand Down
22 changes: 22 additions & 0 deletions src/faction.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,14 @@ std::string fac_respect_text( int val );
std::string fac_wealth_text( int val, int size );
std::string fac_combat_ability_text( int val );

class item;
class JsonIn;
class JsonObject;
class JsonOut;
class faction;
class npc;

struct dialogue;

using faction_id = string_id<faction>;

Expand Down Expand Up @@ -64,6 +68,21 @@ const std::unordered_map<std::string, relationship> relation_strs = { {
};
} // namespace npc_factions

struct faction_price_rules {
itype_id itype;
item_category_id category;
item_group_id item_group;
double markup = 1.0;
cata::optional<double> fixed_adj = cata::nullopt;
std::function<bool( const dialogue & )> condition;

faction_price_rules() = default;
faction_price_rules( itype_id const &id, double m, double f ) : itype( id ), markup( m ),
fixed_adj( f ) {};

void deserialize( JsonObject const &jo );
};

class faction_template
{
protected:
Expand Down Expand Up @@ -91,6 +110,7 @@ class faction_template
int wealth; //Total trade currency
bool lone_wolf_faction; // is this a faction for just one person?
itype_id currency; // id of the faction currency
std::vector<faction_price_rules> price_rules; // additional pricing rules
std::map<std::string, std::bitset<npc_factions::rel_types>> relations;
mfaction_str_id mon_faction; // mon_faction_id of the monster faction; defaults to human
std::set<std::tuple<int, int, snippet_id>> epilogue_data;
Expand All @@ -112,6 +132,8 @@ class faction : public faction_template
std::string food_supply_text();
nc_color food_supply_color();

faction_price_rules const *get_price_rules( item const &it, npc const &guy ) const;

bool has_relationship( const faction_id &guy_id, npc_factions::relationship flag ) const;
void add_to_membership( const character_id &guy_id, const std::string &guy_name, bool known );
void remove_member( const character_id &guy_id );
Expand Down
4 changes: 2 additions & 2 deletions src/npc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2135,13 +2135,13 @@ void npc::update_worst_item_value()
}
}

int npc::value( const item &it ) const
double npc::value( const item &it ) const
{
int market_price = it.price( true );
return value( it, market_price );
}

int npc::value( const item &it, int market_price ) const
double npc::value( const item &it, double market_price ) const
{
if( it.is_dangerous() || ( it.has_flag( flag_BOMB ) && it.active ) ) {
// NPCs won't be interested in buying active explosives
Expand Down
4 changes: 2 additions & 2 deletions src/npc.h
Original file line number Diff line number Diff line change
Expand Up @@ -944,8 +944,8 @@ class npc : public Character
int minimum_item_value() const;
// Find the worst value in our inventory
void update_worst_item_value();
int value( const item &it ) const;
int value( const item &it, int market_price ) const;
double value( const item &it ) const;
double value( const item &it, double market_price ) const;
bool wear_if_wanted( const item &it, std::string &reason );
bool can_read( const item &book, std::vector<std::string> &fail_reasons );
int time_to_read( const item &book, const Character &reader ) const;
Expand Down
19 changes: 14 additions & 5 deletions src/npctrade.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -163,10 +163,15 @@ int npc_trading::bionic_install_price( Character &installer, Character &patient,
int npc_trading::adjusted_price( item const *it, int amount, Character const &buyer,
Character const &seller )
{
double const adjust = npc_trading::net_price_adjustment( buyer, seller );
faction const *const fac = buyer.is_npc() ? buyer.get_faction() : seller.get_faction();
npc const *faction_party = buyer.is_npc() ? buyer.as_npc() : seller.as_npc();
faction_price_rules const *const fpr = fac != nullptr ? fac->get_price_rules( *it,
*faction_party ) : nullptr;

int price = it->price_no_contents( true );
double price = it->price_no_contents( true );
if( fpr != nullptr and seller.is_npc() ) {
price *= fpr->markup;
}
if( it->count_by_charges() and amount >= 0 ) {
price *= static_cast<double>( amount ) / it->charges;
}
Expand All @@ -176,11 +181,15 @@ int npc_trading::adjusted_price( item const *it, int amount, Character const &bu
price = seller.as_npc()->value( *it, price );
}

if( fac == nullptr || fac->currency != it->typeId() ) {
return static_cast<int>( price * ( 1 + 0.25 * adjust ) );
if( fpr != nullptr and fpr->fixed_adj.has_value() ) {
double const fixed_adj = fpr->fixed_adj.value();
price *= 1 + ( seller.is_npc() ? fixed_adj : -fixed_adj );
} else {
double const adjust = npc_trading::net_price_adjustment( buyer, seller );
price *= 1 + 0.25 * adjust;
}

return price;
return static_cast<int>( std::ceil( price ) );
}

int npc_trading::trading_price( Character const &buyer, Character const &seller,
Expand Down
1 change: 1 addition & 0 deletions src/npctrade.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ constexpr char const *VAR_TRADE_IGNORE = "trade_ignore";
class Character;
class item;
class npc;
struct faction_price_rules;
class item_pricing
{
public:
Expand Down
64 changes: 64 additions & 0 deletions tests/faction_price_rules_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#include "avatar.h"
#include "cata_catch.h"
#include "itype.h"
#include "npc.h"
#include "npctrade.h"
#include "player_helpers.h"

static const skill_id skill_speech( "speech" );

TEST_CASE( "faction_price_rules", "[npc][factions][trade]" )
{
clear_avatar();
npc &guy = spawn_npc( { 50, 50 }, "test_npc_trader" );
faction const &fac = *guy.my_fac;

WHEN( "item has no rules (default adjustment)" ) {
item const hammer( "hammer" );
clear_character( guy );
REQUIRE( npc_trading::adjusted_price( &hammer, 1, get_avatar(), guy ) ==
Approx( units::to_cent( hammer.type->price_post ) * 1.25 ).margin( 1 ) );
REQUIRE( npc_trading::adjusted_price( &hammer, 1, guy, get_avatar() ) ==
Approx( units::to_cent( hammer.type->price_post ) * 0.75 ).margin( 1 ) );
}

WHEN( "item is main currency (implicit price rule)" ) {
guy.int_max = 1000;
guy.set_skill_level( skill_speech, 10 );
item const fmcnote( "FMCNote" );
REQUIRE( npc_trading::adjusted_price( &fmcnote, 1, get_avatar(), guy ) ==
units::to_cent( fmcnote.type->price_post ) );
REQUIRE( npc_trading::adjusted_price( &fmcnote, 1, guy, get_avatar() ) ==
units::to_cent( fmcnote.type->price_post ) );
}

WHEN( "item is secondary currency (fixed_adj=0)" ) {
get_avatar().int_max = 1000;
get_avatar().set_skill_level( skill_speech, 10 );
item const pants_fur( "test_pants_fur" );
REQUIRE( npc_trading::adjusted_price( &pants_fur, 1, get_avatar(), guy ) ==
units::to_cent( pants_fur.type->price_post ) );
REQUIRE( npc_trading::adjusted_price( &pants_fur, 1, guy, get_avatar() ) ==
units::to_cent( pants_fur.type->price_post ) );
}

item const carafe( "test_nuclear_carafe" );
WHEN( "condition for price rules not satisfied" ) {
clear_character( guy );
REQUIRE( fac.get_price_rules( carafe, guy ) == nullptr );
REQUIRE( npc_trading::adjusted_price( &carafe, 1, get_avatar(), guy ) ==
Approx( units::to_cent( carafe.type->price_post ) * 1.25 ).margin( 1 ) );
}
WHEN( "condition for price rules satisfied" ) {
guy.set_value( "npctalk_var_bool_allnighter_thirsty", "yes" );
REQUIRE( fac.get_price_rules( carafe, guy )->markup == 2.0 );
THEN( "NPC selling to avatar includes markup and positive fixed adjustment" ) {
REQUIRE( npc_trading::adjusted_price( &carafe, 1, get_avatar(), guy ) ==
Approx( units::to_cent( carafe.type->price_post ) * 2.0 * 1.1 ).margin( 1 ) );
}
THEN( "avatar selling to NPC includes only negative fixed adjustment" ) {
REQUIRE( npc_trading::adjusted_price( &carafe, 1, guy, get_avatar() ) ==
Approx( units::to_cent( carafe.type->price_post ) * 0.9 ).margin( 1 ) );
}
}
}

0 comments on commit 766497b

Please sign in to comment.