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

JSON-ize more shopkeeper attributes #57952

Merged
merged 3 commits into from
Jun 17, 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
19 changes: 18 additions & 1 deletion data/mods/TEST_DATA/npc_shop_cons_rates.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
"junk_threshold": "10 cent",
"rates": [
{ "item": "bow_saw", "rate": 2 },
{
"item": "bow_saw",
"rate": 99,
"condition": { "npc_has_var": "bow_saw_eater", "type": "bool", "context": "dinner", "value": "yes" }
},
{ "category": "currency", "rate": 10 },
{ "group": "test_event_item_spawn", "rate": 100 },
{ "group": "test_event_item_spawn", "category": "tools", "rate": 50 }
Expand All @@ -17,6 +22,16 @@
"type": "shopkeeper_consumption_rates",
"extend": { "rates": [ { "item": "FMCNote", "rate": 25 } ] }
},
{
"id": "test_blacklist",
"type": "shopkeeper_blacklist",
"entries": [
{
"item": "bow_saw",
"condition": { "npc_has_var": "hates_bow_saws", "type": "bool", "context": "bigotry", "value": "yes" }
}
]
},
{
"type": "npc_class",
"id": "test_npc_trader_class",
Expand All @@ -34,7 +49,9 @@
"strict": true,
"condition": { "u_has_var": "multitool_access", "type": "bool", "context": "test", "value": "yes" }
}
]
],
"shopkeeper_blacklist": "test_blacklist",
"restock_interval": "99 days"
},
{
"type": "npc",
Expand Down
23 changes: 23 additions & 0 deletions doc/JSON_INFO.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ Use the `Home` key to return to the top.
- [`points`](#points)
- [`addictions`](#addictions)
- [`shopkeeper_consumption_rates`](#shopkeeper_consumption_rates)
- [`shopkeeper_blacklist`](#shopkeeper_blacklist)
- [`skills`](#skills)
- [`missions`](#missions)
- [`proficiencies`](#proficiencies)
Expand Down Expand Up @@ -1458,11 +1459,33 @@ Example:
"junk_threshold": "10 cent", // items below this price will be consumed completely regardless of matches below
"rates": [ // lower entries override higher ones
{ "item": "hammer", "rate": 1 },
{
"item": "hammer",
"rate": 10,
"condition": { "npc_has_var": "hammer_eater", "type": "bool", "context": "dinner", "value": "yes" }
},
{ "category": "ammo", "rate": 10 },
{ "group": "EXODII_basic_trade", "rate": 100 }
{ "group": "EXODII_basic_trade", "category": "ammo", "rate": 200 }
]
```
`condition` is checked with avatar as alpha and npc as beta. See [Player or NPC conditions](NPCs.md#player-or-npc-conditions).

#### `shopkeeper_blacklist`
Similar to `shopkeeper_consumption_rates`

```JSON
"type": "shopkeeper_blacklist",
"id": "basic_blacklist",
"entries": [
{
"item": "hammer",
"condition": { "npc_has_var": "hammer_hater", "type": "bool", "context": "test", "value": "yes" }
},
{ "category": "ammo" },
{ "group": "EXODII_basic_trade" }
]
```

#### `skills`

Expand Down
4 changes: 4 additions & 0 deletions doc/NPCs.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ Format:
}
],
"shopkeeper_consumption_rates": "basic_shop_rates",
"shopkeeper_blacklist": "test_blacklist",
"restock_interval": "6 days",
"traits": [ { "group": "BG_survival_story_EVACUEE" }, { "group": "NPC_starting_traits" }, { "group": "Appearance_demographics" } ]
}
```
Expand All @@ -45,6 +47,8 @@ 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_blacklist"` optional to define blacklists for this shopkeeper
* `"restock_interval"`: optional. Default is 6 days

##### Shopkeeper item groups
`"shopkeeper_item_group"` entries have the following fields:
Expand Down
2 changes: 2 additions & 0 deletions src/init.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ void DynamicDataLoader::initialize()
add( "start_location", &start_locations::load );
add( "scenario", &scenario::load_scenario );
add( "SCENARIO_BLACKLIST", &scen_blacklist::load_scen_blacklist );
add( "shopkeeper_blacklist", &shopkeeper_blacklist::load_blacklist );
add( "shopkeeper_consumption_rates", &shopkeeper_cons_rates::load_rate );
add( "skill_boost", &skill_boost::load_boost );
add( "enchantment", &enchantment::load_enchantment );
Expand Down Expand Up @@ -624,6 +625,7 @@ void DynamicDataLoader::unload_data()
scenario::reset();
scent_type::reset();
score::reset();
shopkeeper_blacklist::reset();
shopkeeper_cons_rates::reset();
Skill::reset();
skill_boost::reset();
Expand Down
6 changes: 5 additions & 1 deletion src/npc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1959,6 +1959,10 @@ bool npc::wants_to_buy( const item &it, int at_price, int /*market_price*/ ) con
return false;
}

if( myclass->get_shopkeeper_blacklist().matches( it, *this ) ) {
return false;
}

// TODO: Base on inventory
return at_price >= 0;
}
Expand Down Expand Up @@ -2026,7 +2030,7 @@ void npc::shop_restock()
return;
}

restock = calendar::turn + 6_days;
restock = calendar::turn + myclass->get_shop_restock_interval();
if( is_player_ally() ) {
return;
}
Expand Down
17 changes: 17 additions & 0 deletions src/npc_class.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,9 @@ void npc_class::load( const JsonObject &jo, const std::string & )
}
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,
shopkeeper_blacklist_id::NULL_ID() );
optional( jo, was_loaded, "restock_interval", restock_interval, 6_days );
optional( jo, was_loaded, "worn_override", worn_override );
optional( jo, was_loaded, "carry_override", carry_override );
optional( jo, was_loaded, "weapon_override", weapon_override );
Expand Down Expand Up @@ -408,6 +411,20 @@ const shopkeeper_cons_rates &npc_class::get_shopkeeper_cons_rates() const
return shop_cons_rates_id.obj();
}

const shopkeeper_blacklist &npc_class::get_shopkeeper_blacklist() const
{
if( shop_blacklist_id.is_null() ) {
shopkeeper_blacklist static const null_blacklist;
return null_blacklist;
}
return shop_blacklist_id.obj();
}

const time_duration &npc_class::get_shop_restock_interval() const
{
return restock_interval;
}

int npc_class::roll_strength() const
{
return dice( 4, 3 ) + bonus_str.roll();
Expand Down
4 changes: 4 additions & 0 deletions src/npc_class.h
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ class npc_class
// first -> item group, second -> trust
std::vector<shopkeeper_item_group> shop_item_groups;
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;

public:
npc_class_id id;
Expand Down Expand Up @@ -118,6 +120,8 @@ class npc_class

const std::vector<shopkeeper_item_group> &get_shopkeeper_items() const;
const shopkeeper_cons_rates &get_shopkeeper_cons_rates() const;
const shopkeeper_blacklist &get_shopkeeper_blacklist() const;
const time_duration &get_shop_restock_interval() const;

void load( const JsonObject &jo, const std::string &src );

Expand Down
2 changes: 1 addition & 1 deletion src/npctrade_utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ void _consume_item( item_location elem, consume_queue &consumed, consume_cache &
if( contents.empty() ) {
auto it = cache.find( elem->typeId() );
if( it == cache.end() ) {
int const rate = guy.myclass->get_shopkeeper_cons_rates().get_rate( *elem );
int const rate = guy.myclass->get_shopkeeper_cons_rates().get_rate( *elem, guy );
int const rate_init = rate >= 0 ? rate * to_days<int>( elapsed ) : -1;
it = cache.emplace( elem->typeId(), rate_init ).first;
}
Expand Down
106 changes: 93 additions & 13 deletions src/shop_cons_rate.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#include "shop_cons_rate.h"

#include "avatar.h"
#include "condition.h"
#include "generic_factory.h"
#include "item_category.h"
#include "item_group.h"
Expand All @@ -10,9 +12,49 @@ namespace
{

generic_factory<shopkeeper_cons_rates> shop_cons_rate_factory( SHOPKEEPER_CONSUMPTION_RATES );
generic_factory<shopkeeper_blacklist> shop_blacklist_factory( SHOPKEEPER_BLACKLIST );

} // namespace

bool icg_entry::operator==( icg_entry const &rhs ) const
{
return itype == rhs.itype and category == rhs.category and item_group == rhs.item_group;
}

bool icg_entry::matches( item const &it, npc const &beta ) const
{
dialogue const temp( get_talker_for( get_avatar() ), get_talker_for( beta ) );
return ( !condition or condition( temp ) ) and
( itype.is_empty() or it.typeId() == itype ) and
( category.is_empty() or it.get_category_shallow().id == category ) and
( item_group.is_empty() or
item_group::group_contains_item( item_group, it.typeId() ) );
}

/** @relates string_id */
template<>
shopkeeper_blacklist const &string_id<shopkeeper_blacklist>::obj() const
{
return shop_blacklist_factory.obj( *this );
}

/** @relates string_id */
template<>
bool string_id<shopkeeper_blacklist>::is_valid() const
{
return shop_blacklist_factory.is_valid( *this );
}

void shopkeeper_blacklist::reset()
{
shop_blacklist_factory.reset();
}

std::vector<shopkeeper_blacklist> const &shopkeeper_blacklist::get_all()
{
return shop_blacklist_factory.get_all();
}

/** @relates string_id */
template<>
shopkeeper_cons_rates const &string_id<shopkeeper_cons_rates>::obj() const
Expand Down Expand Up @@ -42,29 +84,55 @@ void shopkeeper_cons_rates::load_rate( const JsonObject &jo, std::string const &
shop_cons_rate_factory.load( jo, src );
}

void shopkeeper_blacklist::load_blacklist( const JsonObject &jo, std::string const &src )
{
shop_blacklist_factory.load( jo, src );
}

void shopkeeper_cons_rates::check_all()
{
shop_cons_rate_factory.check();
}

class shopkeeper_cons_rates_reader : public generic_typed_reader<shopkeeper_cons_rates_reader>
class icg_entry_reader : public generic_typed_reader<icg_entry_reader>
{
public:
static shopkeeper_cons_rate_entry get_next( JsonValue &jv ) {
JsonObject jo = jv.get_object();
shopkeeper_cons_rate_entry ret;
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;
}
};

class shopkeeper_cons_rates_reader : public generic_typed_reader<shopkeeper_cons_rates_reader>
{
public:
static shopkeeper_cons_rate_entry get_next( JsonValue &jv ) {
JsonObject jo = jv.get_object();
shopkeeper_cons_rate_entry ret( icg_entry_reader::_part_get_next( jo ) );
mandatory( jo, false, "rate", ret.rate );
return ret;
}
};

bool shopkeeper_cons_rate_entry::operator==( shopkeeper_cons_rate_entry const &rhs ) const
{
return itype == rhs.itype and category == rhs.category and item_group == rhs.item_group and
rate == rhs.rate;
return icg_entry::operator==( rhs ) and rate == rhs.rate;
}

void shopkeeper_blacklist::load( JsonObject const &jo, std::string const &/*src*/ )
{
optional( jo, was_loaded, "entries", entries, icg_entry_reader {} );
}

void shopkeeper_cons_rates::load( JsonObject const &jo, std::string const &/*src*/ )
Expand All @@ -88,20 +156,32 @@ void shopkeeper_cons_rates::check() const
}
}

int shopkeeper_cons_rates::get_rate( item const &it ) const
int shopkeeper_cons_rates::get_rate( item const &it, npc const &beta ) const
{
if( it.type->price_post < junk_threshold ) {
return -1;
}
for( auto rit = rates.crbegin(); rit != rates.crend(); ++rit ) {
bool const has =
( rit->itype.is_empty() or it.typeId() == rit->itype ) and
( rit->category.is_empty() or it.get_category_shallow().id == rit->category ) and
( rit->item_group.is_empty() or
item_group::group_contains_item( rit->item_group, it.typeId() ) );
if( has ) {
if( rit->matches( it, beta ) ) {
return rit->rate;
}
}
return default_rate;
}

bool shopkeeper_blacklist::matches( item const &it, npc const &beta ) const
{
return std::any_of( entries.begin(), entries.end(),
[&it, &beta]( icg_entry const & rit ) {
return rit.matches( it, beta );
} );
}

bool shopkeeper_cons_rates::matches( item const &it, npc const &beta ) const
{
return it.type->price_post < junk_threshold or
std::any_of( rates.begin(), rates.end(),
[&it, &beta]( shopkeeper_cons_rate_entry const & rit ) {
return rit.matches( it, beta );
} );
}
Loading