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

factions: allow defining custom price rules #58141

Merged
merged 1 commit into from
Jun 18, 2022
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
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": [ { "item": "money_strap_FMCNote", "fixed_adj": 0 }, { "item": "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": [
{ "item": "test_pants_fur", "fixed_adj": 0 },
{
"item": "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 @@ -57,8 +57,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": [
{ "item": "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 `item`/`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
28 changes: 28 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 @@ -107,6 +109,17 @@ 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>
{
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;
}
};

faction_template::faction_template( const JsonObject &jsobj )
: name( jsobj.get_string( "name" ) )
Expand All @@ -121,8 +134,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, faction_price_rules_reader {} );
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 +356,18 @@ nc_color faction::food_supply_color()
}
}

faction_price_rule const *faction::get_price_rules( item const &it, npc const &guy ) const
{
auto const el = std::find_if(
price_rules.crbegin(), price_rules.crend(), [&it, &guy]( faction_price_rule const & fc ) {
return fc.matches( it, guy );
} );
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 +467,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
20 changes: 20 additions & 0 deletions src/faction.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

#include "character_id.h"
#include "color.h"
#include "shop_cons_rate.h"
#include "translations.h"
#include "type_id.h"

Expand All @@ -30,10 +31,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 +69,18 @@ const std::unordered_map<std::string, relationship> relation_strs = { {
};
} // namespace npc_factions

struct faction_price_rule: public icg_entry {
double markup = 1.0;
cata::optional<double> fixed_adj = cata::nullopt;

faction_price_rule() = default;
faction_price_rule( itype_id const &id, double m, double f )
: icg_entry{ id, {}, {}, {} }, markup( m ), fixed_adj( f ) {};
explicit faction_price_rule( icg_entry const &rhs ) : icg_entry( rhs ) {}

void deserialize( JsonObject const &jo );
};

class faction_template
{
protected:
Expand Down Expand Up @@ -91,6 +108,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_rule> 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 +130,8 @@ class faction : public faction_template
std::string food_supply_text();
nc_color food_supply_color();

faction_price_rule 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 @@ -2138,13 +2138,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_rule 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_rule;
class item_pricing
{
public:
Expand Down
34 changes: 16 additions & 18 deletions src/shop_cons_rate.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -94,25 +94,23 @@ void shopkeeper_cons_rates::check_all()
shop_cons_rate_factory.check();
}

class icg_entry_reader : public generic_typed_reader<icg_entry_reader>
icg_entry icg_entry_reader::_part_get_next( JsonObject const &jo )
{
icg_entry ret;
optional( jo, false, "item", ret.itype );
optional( jo, false, "category", ret.category );
optional( jo, false, "group", ret.item_group );
if( jo.has_member( "condition" ) ) {
read_condition<dialogue>( jo, "condition", ret.condition, false );
}
return ret;
}
icg_entry icg_entry_reader::get_next( JsonValue &jv )
{
public:
static icg_entry _part_get_next( JsonObject const &jo ) {
icg_entry ret;
optional( jo, false, "item", ret.itype );
optional( jo, false, "category", ret.category );
optional( jo, false, "group", ret.item_group );
if( jo.has_member( "condition" ) ) {
read_condition<dialogue>( jo, "condition", ret.condition, false );
}
return ret;
}
static icg_entry get_next( JsonValue &jv ) {
JsonObject jo = jv.get_object();
icg_entry ret( _part_get_next( jo ) );
return ret;
}
};
JsonObject jo = jv.get_object();
icg_entry ret( _part_get_next( jo ) );
return ret;
}

class shopkeeper_cons_rates_reader : public generic_typed_reader<shopkeeper_cons_rates_reader>
{
Expand Down
11 changes: 10 additions & 1 deletion src/shop_cons_rate.h
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
#ifndef CATA_SRC_SHOP_CONS_RATE_H
#define CATA_SRC_SHOP_CONS_RATE_H

#include "dialogue.h"
#include "generic_factory.h"
#include "type_id.h"
#include "units.h"

class JsonObject;
class npc;
struct dialogue;

constexpr char const *SHOPKEEPER_CONSUMPTION_RATES = "shopkeeper_consumption_rates";
constexpr char const *SHOPKEEPER_BLACKLIST = "shopkeeper_blacklist";
Expand All @@ -21,6 +23,13 @@ struct icg_entry {
bool matches( item const &it, npc const &beta ) const;
};

class icg_entry_reader : public generic_typed_reader<icg_entry_reader>
{
public:
static icg_entry _part_get_next( JsonObject const &jo );
static icg_entry get_next( JsonValue &jv );
};

struct shopkeeper_cons_rate_entry: public icg_entry {
int rate = 0;

Expand Down
Loading