npc: add support for personal price rules

This commit is contained in:
andrei 2022-08-02 21:55:48 +03:00
parent fe457b7a99
commit fb88068cb9
10 changed files with 59 additions and 14 deletions

View File

@ -51,6 +51,13 @@
}
],
"shopkeeper_blacklist": "test_blacklist",
"shopkeeper_price_rules": [
{
"item": "test_pants_fur",
"markup": -100,
"condition": { "npc_has_var": "vegan", "type": "bool", "context": "preference", "value": "yes" }
}
],
"restock_interval": "99 days"
},
{

View File

@ -37,6 +37,9 @@ Format:
}
],
"shopkeeper_consumption_rates": "basic_shop_rates",
"shopkeeper_price_rules": [
{ "item": "scrap", "fixed_price": 10000 },
]
"shopkeeper_blacklist": "test_blacklist",
"restock_interval": "6 days",
"traits": [ { "group": "BG_survival_story_EVACUEE" }, { "group": "NPC_starting_traits" }, { "group": "Appearance_demographics" } ]
@ -47,6 +50,7 @@ There are a couple of items in the above template that may not be self explanato
* `"sells_belongings": false` means that this NPC's worn or held items will strictly be excluded from their shopkeeper list; otherwise, they'll be happy to sell things like their pants. It defaults to `true` if not specified.
*`"shopkeeper_item_group"` is only needed if the planned NPC will be a shopkeeper with a revolving stock of items that change every three in-game days. All of the item overrides will ensure that any NPC of this class spawns with specific items.
* `"shopkeeper_consumption_rates"` optional to define item consumption rates for this shopkeeper. Default is to consume all items before restocking
* `"shopkeeper_price_rules"` optional to define personal price rules with the same format as faction price rules (see [FACTIONS.md](FACTIONS.md)). These take priority over faction rules
* `"shopkeeper_blacklist"` optional to define blacklists for this shopkeeper
* `"restock_interval"`: optional. Default is 6 days

View File

@ -109,17 +109,14 @@ void faction_template::load_relations( const JsonObject &jsobj )
relations[fac.name()] = fac_relation;
}
}
class faction_price_rules_reader : public generic_typed_reader<faction_price_rules_reader>
faction_price_rule faction_price_rules_reader::get_next( JsonValue &jv )
{
public:
static faction_price_rule get_next( JsonValue &jv ) {
JsonObject jo = jv.get_object();
faction_price_rule ret( icg_entry_reader::_part_get_next( jo ) );
optional( jo, false, "markup", ret.markup, 1.0 );
optional( jo, false, "fixed_adj", ret.fixed_adj, cata::nullopt );
return ret;
}
};
JsonObject jo = jv.get_object();
faction_price_rule ret( icg_entry_reader::_part_get_next( jo ) );
optional( jo, false, "markup", ret.markup, 1.0 );
optional( jo, false, "fixed_adj", ret.fixed_adj, cata::nullopt );
return ret;
}
faction_template::faction_template( const JsonObject &jsobj )
: name( jsobj.get_string( "name" ) )

View File

@ -81,6 +81,12 @@ struct faction_price_rule: public icg_entry {
void deserialize( JsonObject const &jo );
};
class faction_price_rules_reader : public generic_typed_reader<faction_price_rules_reader>
{
public:
static faction_price_rule get_next( JsonValue &jv );
};
class faction_template
{
protected:

View File

@ -2260,6 +2260,15 @@ double npc::value( const item &it, double market_price ) const
return std::round( ret * market_price );
}
faction_price_rule const *npc::get_price_rules( item const &it ) const
{
faction_price_rule const *ret = myclass->get_price_rules( it, *this );
if( ret == nullptr && get_faction() != nullptr ) {
ret = get_faction()->get_price_rules( it, *this );
}
return ret;
}
void healing_options::clear_all()
{
bandage = false;

View File

@ -908,6 +908,7 @@ class npc : public Character
void update_worst_item_value();
double value( const item &it ) const;
double value( const item &it, double market_price ) const;
faction_price_rule const *get_price_rules( item const &it ) const;
bool wear_if_wanted( const item &it, std::string &reason );
bool can_read( const item &book, std::vector<std::string> &fail_reasons );
time_duration time_to_read( const item &book, const Character &reader ) const;

View File

@ -289,6 +289,7 @@ void npc_class::load( const JsonObject &jo, const std::string & )
jo.throw_error( string_format( "invalid format for shopkeeper_item_group in npc class %s", name ) );
}
}
optional( jo, was_loaded, "shopkeeper_price_rules", shop_price_rules, faction_price_rules_reader {} );
optional( jo, was_loaded, SHOPKEEPER_CONSUMPTION_RATES, shop_cons_rates_id,
shopkeeper_cons_rates_id::NULL_ID() );
optional( jo, was_loaded, SHOPKEEPER_BLACKLIST, shop_blacklist_id,
@ -430,6 +431,18 @@ const shopkeeper_blacklist &npc_class::get_shopkeeper_blacklist() const
return shop_blacklist_id.obj();
}
faction_price_rule const *npc_class::get_price_rules( item const &it, npc const &guy ) const
{
auto const el = std::find_if(
shop_price_rules.crbegin(), shop_price_rules.crend(), [&it, &guy]( faction_price_rule const & fc ) {
return fc.matches( it, guy );
} );
if( el != shop_price_rules.crend() ) {
return &*el;
}
return nullptr;
}
const time_duration &npc_class::get_shop_restock_interval() const
{
return restock_interval;

View File

@ -16,6 +16,7 @@ class JsonObject;
class Trait_group;
struct dialogue;
struct faction_price_rule;
namespace trait_group
{
@ -86,6 +87,7 @@ class npc_class
// first -> item group, second -> trust
std::vector<shopkeeper_item_group> shop_item_groups;
std::vector<faction_price_rule> shop_price_rules;
shopkeeper_cons_rates_id shop_cons_rates_id = shopkeeper_cons_rates_id::NULL_ID();
shopkeeper_blacklist_id shop_blacklist_id = shopkeeper_blacklist_id::NULL_ID();
time_duration restock_interval = 6_days;
@ -124,6 +126,7 @@ class npc_class
const shopkeeper_cons_rates &get_shopkeeper_cons_rates() const;
const shopkeeper_blacklist &get_shopkeeper_blacklist() const;
const time_duration &get_shop_restock_interval() const;
faction_price_rule const *get_price_rules( item const &it, npc const &guy ) const;
void load( const JsonObject &jo, const std::string &src );

View File

@ -161,10 +161,8 @@ 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 )
{
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_rule const *const fpr = fac != nullptr ? fac->get_price_rules( *it,
*faction_party ) : nullptr;
faction_price_rule const *const fpr = faction_party->get_price_rules( *it );
double price = it->price_no_contents( true );
if( fpr != nullptr && seller.is_npc() ) {

View File

@ -32,10 +32,10 @@ TEST_CASE( "faction_price_rules", "[npc][factions][trade]" )
units::to_cent( fmcnote.type->price_post ) );
}
item const pants_fur( "test_pants_fur" );
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() ) ==
@ -61,4 +61,11 @@ TEST_CASE( "faction_price_rules", "[npc][factions][trade]" )
Approx( units::to_cent( carafe.type->price_post ) * 0.9 ).margin( 1 ) );
}
}
WHEN( "personal price rule overrides faction rule" ) {
double const fmarkup = fac.get_price_rules( pants_fur, guy )->markup;
REQUIRE( guy.get_price_rules( pants_fur )->markup == fmarkup );
REQUIRE( fmarkup != - 100 );
guy.set_value( "npctalk_var_bool_preference_vegan", "yes" );
REQUIRE( guy.get_price_rules( pants_fur )->markup == -100 );
}
}