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

NPC trade - bugfixes and improvements #51704

Merged
merged 2 commits into from
Sep 28, 2021
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
39 changes: 38 additions & 1 deletion src/item.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5575,7 +5575,7 @@ int item::price( bool practical ) const

} else if( e->magazine_integral() && e->ammo_remaining() && e->ammo_data() ) {
// items with integral magazines may contain ammunition which can affect the price
child += item( e->ammo_data(), calendar::turn, e->charges ).price( practical );
child += item( e->ammo_data(), calendar::turn, e->ammo_remaining() ).price( practical );

} else if( e->is_tool() && e->type->tool->max_charges != 0 ) {
// if tool has no ammo (e.g. spray can) reduce price proportional to remaining charges
Expand All @@ -5589,6 +5589,43 @@ int item::price( bool practical ) const
return res;
}

int item::price_no_contents( bool practical )
{
if( rotten() ) {
return 0;
}
int price = units::to_cent( practical ? type->price_post : type->price );
if( damage() > 0 ) {
// maximal damage level is 4, maximal reduction is 40% of the value.
price -= price * static_cast< double >( damage_level() ) / 10;
}

if( count_by_charges() || made_of( phase_id::LIQUID ) ) {
// price from json data is for default-sized stack
price *= charges / static_cast< double >( type->stack_size );

} else if( ( magazine_integral() || is_magazine() ) && ammo_remaining() && ammo_data() ) {
// items with integral magazines may contain ammunition which can affect the price
price += item( ammo_data(), calendar::turn, ammo_remaining() ).price( practical );

} else if( is_tool() && type->tool->max_charges != 0 ) {
// if tool has no ammo (e.g. spray can) reduce price proportional to remaining charges
price *= ammo_remaining() / static_cast< double >( std::max( type->charges_default(), 1 ) );

} else if( is_watertight_container() ) {
// Liquid contents are hidden so must be included in the price.
visit_contents( [&price, &practical]( item * node, item * ) {
if( node->type->phase != phase_id::LIQUID ) {
return VisitResponse::SKIP;
}
price += node->price_no_contents( practical );
return VisitResponse::SKIP;
} );
}

return price;
}

// TODO: MATERIALS add a density field to materials.json
units::mass item::weight( bool include_contents, bool integral ) const
{
Expand Down
8 changes: 8 additions & 0 deletions src/item.h
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,14 @@ class item : public visitable
*/
int price( bool practical ) const;

/**
* Returns the monetary value of an item by itself.
* Price includes hidden contents such as ammo and liquids.
* If `practical` is false, returns pre-cataclysm market value,
* otherwise returns approximate post-cataclysm value.
*/
int price_no_contents( bool practical );

/**
* Whether two items should stack when displayed in a inventory menu.
* This is different from stacks_with, when two previously non-stackable
Expand Down
178 changes: 133 additions & 45 deletions src/npctrade.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ std::list<item> npc_trading::transfer_items( std::vector<item_pricing> &stuff, C
const bool use_escrow = !npc_gives;
std::list<item> escrow = std::list<item>();

// Sort top level containers to be processed last.
// Used to prevent character handling of contained items that are also traded.
std::vector<std::reference_wrapper<item_pricing>> unsorted_stuff;
std::vector<std::reference_wrapper<item_pricing>> containers;
std::vector<std::reference_wrapper<item_pricing>> sorted_stuff;
for( item_pricing &ip : stuff ) {
if( !ip.selected ) {
continue;
Expand All @@ -59,11 +64,49 @@ std::list<item> npc_trading::transfer_items( std::vector<item_pricing> &stuff, C
continue;
}

if( ip.loc.get_item()->is_container() ) {
containers.emplace_back( ip );
} else {
unsorted_stuff.emplace_back( ip );
}
}
// Sort the containers only. Non-containers do not need to be sorted.
for( item_pricing &cont : containers ) {
for( std::vector<std::reference_wrapper<item_pricing>>::iterator iter = sorted_stuff.begin();
iter != sorted_stuff.end(); ++iter ) {
if( cont.loc.has_parent() && cont.loc.parent_item() == iter->get().loc ) {
sorted_stuff.insert( iter, cont );
break;
}
}
sorted_stuff.emplace_back( cont );
}
sorted_stuff.insert( sorted_stuff.begin(), unsorted_stuff.begin(), unsorted_stuff.end() );

for( item_pricing ip : sorted_stuff ) {

if( ip.loc.get_item() == nullptr ) {
DebugLog( D_ERROR, D_NPC ) << "Null item being traded in npc_trading::transfer_items";
continue;
}

item gift = *ip.loc.get_item();
gift.set_owner( receiver );
int charges = npc_gives ? ip.u_charges : ip.npc_charges;
int count = npc_gives ? ip.u_has : ip.npc_has;

// Only affects worn containers. Other containers have contents hidden.
// Only untraded contents remain due to sorting.
if( gift.is_container() && !gift.is_tool() && !gift.is_firearm() &&
ip.loc.where() == item_location::type::character ) {
for( item *it : gift.get_contents().all_items_top() ) {
if( it->made_of_from_type( phase_id::SOLID ) ) {
giver.i_add_or_drop( *it, 1, ip.loc.get_item() );
gift.remove_item( *it );
}
}
}

// Items are moving to escrow.
if( use_escrow && ip.charges ) {
gift.charges = charges;
Expand All @@ -80,13 +123,13 @@ std::list<item> npc_trading::transfer_items( std::vector<item_pricing> &stuff, C
}
}

if( ip.loc.where() == item_location::type::character ) {
if( ip.loc.held_by( giver ) ) {
if( ip.charges > 0 ) {
giver.use_charges( gift.typeId(), charges );
} else if( ip.count > 0 ) {
for( int i = 0; i < count; i++ ) {
giver.use_amount( gift.typeId(), 1 );
}
giver.remove_items_with( [&ip]( const item & i ) {
return &i == ip.loc.get_item();
}, count );
}
} else {
if( ip.charges > 0 ) {
Expand Down Expand Up @@ -175,8 +218,8 @@ std::vector<item_pricing> npc_trading::init_buying( Character &buyer, Character
}
item &it = *loc;

// Don't sell items that are loose liquid
if( it.made_of( phase_id::LIQUID ) ) {
// Only solids allowed. All others should be transfered in a container.
if( !it.made_of( phase_id::SOLID ) ) {
return;
}

Expand All @@ -190,7 +233,16 @@ std::vector<item_pricing> npc_trading::init_buying( Character &buyer, Character
return;
}

const int market_price = it.price( true );
// Hide contents of any containers that are not worn.
if( loc.has_parent() && !( loc.parent_item().where() == item_location::type::character ) ) {
return;
}

// Worn containers have most contents visible so they show price for the container only,
// except hidden contents such as liquids.
const int market_price = loc.where() == item_location::type::character ?
it.price_no_contents( true ) :
it.price( true );
int val = np.value( it, market_price );
if( ( is_npc && np.wants_to_sell( it, val, market_price ) ) ||
( !is_npc && np.wants_to_buy( it, val, market_price ) ) ) {
Expand Down Expand Up @@ -630,49 +682,85 @@ bool trading_window::perform_trade( npc &np, const std::string &deal )

ch += offset;
if( ch < target_list.size() ) {
item_pricing &ip = target_list[ch];
int change_amount = 1;
int &owner_sells = focus_them ? ip.u_has : ip.npc_has;
int &owner_sells_charge = focus_them ? ip.u_charges : ip.npc_charges;

if( ip.selected ) {
if( owner_sells_charge > 0 ) {
change_amount = owner_sells_charge;
owner_sells_charge = 0;
} else if( owner_sells > 0 ) {
change_amount = owner_sells;
owner_sells = 0;
item_pricing &ipr = target_list[ch];

// Recursive lambda https://artificial-mind.net/blog/2020/09/12/recursive-lambdas
auto item_selection = [ this, &np, &target_list ]( item_pricing & ip,
auto &&item_selection, bool max = false ) -> void {
int change_amount = 1;
int &owner_sells = focus_them ? ip.u_has : ip.npc_has;
int &owner_sells_charge = focus_them ? ip.u_charges : ip.npc_charges;

if( ip.selected )
{
if( owner_sells_charge > 0 ) {
change_amount = owner_sells_charge;
owner_sells_charge = 0;
} else if( owner_sells > 0 ) {
change_amount = owner_sells;
owner_sells = 0;
// Deselect all contents when deselecting a container.
if( ip.is_container ) {
for( item *it : ip.loc.get_item()->get_contents().all_items_top() ) {
for( item_pricing &ipp : target_list ) {
if( it == ipp.loc.get_item() && ipp.selected ) {
item_selection( ipp, item_selection );
break;
}
}
}
}
}
} else if( ip.charges > 0 )
{
change_amount = max ? ip.charges : get_var_trade( *ip.loc.get_item(), ip.charges );

if( change_amount < 1 ) {
return;
}
owner_sells_charge = change_amount;
} else
{
if( ip.count > 1 ) {
change_amount = max ? ip.count : get_var_trade( *ip.loc.get_item(), ip.count );

if( change_amount < 1 ) {
return;
}
}
owner_sells = change_amount;
// Select all contents when selecting a container.
if( ip.is_container ) {
for( item *it : ip.loc.get_item()->get_contents().all_items_top() ) {
for( item_pricing &ipp : target_list ) {
if( it == ipp.loc.get_item() && !ipp.selected ) {
item_selection( ipp, item_selection, true );
break;
}
}
}
}
}
} else if( ip.charges > 0 ) {
change_amount = get_var_trade( *ip.loc.get_item(), ip.charges );
if( change_amount < 1 ) {
continue;
ip.selected = !ip.selected;
if( ip.selected != focus_them )
{
change_amount *= -1;
}
owner_sells_charge = change_amount;
} else {
if( ip.count > 1 ) {
change_amount = get_var_trade( *ip.loc.get_item(), ip.count );
if( change_amount < 1 ) {
continue;
int delta_price = ip.price * change_amount;
if( !np.will_exchange_items_freely() )
{
your_balance -= delta_price;
if( ip.selected != focus_them ) {
your_sale_value -= delta_price;
}
}
owner_sells = change_amount;
}
ip.selected = !ip.selected;
if( ip.selected != focus_them ) {
change_amount *= -1;
}
int delta_price = ip.price * change_amount;
if( !np.will_exchange_items_freely() ) {
your_balance -= delta_price;
if( ip.selected != focus_them ) {
your_sale_value -= delta_price;
if( ip.loc.where_recursive() == item_location::type::character )
{
volume_left += ip.vol * change_amount;
weight_left += ip.weight * change_amount;
}
}
if( ip.loc.where() == item_location::type::character ) {
volume_left += ip.vol * change_amount;
weight_left += ip.weight * change_amount;
}
};
item_selection( ipr, item_selection );
}
}
}
Expand Down