Skip to content

Commit

Permalink
Port over salvage-by-weight from DDA (cataclysmbnteam#2142)
Browse files Browse the repository at this point in the history
* Port over salvage-by-weight from DDA

Co-Authored-By: Kevin Granade <860276+kevingranade@users.noreply.github.com>

* Commit test for now

* Update iuse_actor_test.cpp

* Update iuse_actor_test.cpp

* Update iuse_actor_test.cpp

* Updates per feedback

* Check all items if they cut up properly

* Update tests/iuse_actor_test.cpp

Co-authored-by: Olanti <olanti-p@yandex.ru>

---------

Co-authored-by: Kevin Granade <860276+kevingranade@users.noreply.github.com>
Co-authored-by: Coolthulhu <Coolthulhu@gmail.com>
Co-authored-by: Olanti <olanti-p@yandex.ru>
Co-authored-by: scarf <greenscarf005@gmail.com>
  • Loading branch information
5 people authored Sep 23, 2023
1 parent 744bfdb commit 5afebd6
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 23 deletions.
2 changes: 1 addition & 1 deletion data/json/items/armor/pets_small_quadruped_armor.json
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@
"price": 5000,
"price_postapoc": 500,
"material": [ "neoprene", "plastic" ],
"weight": "2500 mg",
"weight": "50 g",
"volume": "4500 ml",
"material_thickness": 3
},
Expand Down
2 changes: 1 addition & 1 deletion data/json/items/armor/swimming.json
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,6 @@
"warmth": 30,
"material_thickness": 1,
"environmental_protection": 1,
"flags": [ "VARSIZE", "WATER_FRIENDLY", "SKINTIGHT" ]
"flags": [ "VARSIZE", "WATER_FRIENDLY", "SKINTIGHT", "NO_SALVAGE" ]
}
]
82 changes: 61 additions & 21 deletions src/iuse_actor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1565,11 +1565,40 @@ int salvage_actor::use( player &p, item &it, bool t, const tripoint & ) const
return cut_up( p, it, item_loc );
}

static const units::volume minimal_volume_to_cut = 250_ml;
// Helper to visit instances of all the sub-materials of an item.
static void visit_salvage_products( const item &it, std::function<void( const item & )> func )
{
for( const material_id &material : it.made_of() ) {
if( const std::optional<itype_id> id = material->salvaged_into() ) {
item exemplar( *id );
func( exemplar );
}
}
}

// Helper to find smallest sub-component of an item.
static units::mass minimal_weight_to_cut( const item &it )
{
units::mass min_weight = units::mass_max;
visit_salvage_products( it, [&min_weight]( const item & exemplar ) {
min_weight = std::min( min_weight, exemplar.weight() );
} );
return min_weight;
}

int salvage_actor::time_to_cut_up( const item &it ) const
{
int count = it.volume() / minimal_volume_to_cut;
units::mass total_material_weight;
int num_materials = 0;
visit_salvage_products( it, [&total_material_weight, &num_materials]( const item & exemplar ) {
total_material_weight += exemplar.weight();
num_materials += 1;
} );
if( num_materials == 0 ) {
return 0;
}
units::mass average_material_weight = total_material_weight / num_materials;
int count = it.weight() / average_material_weight;
return moves_per_part * count;
}

Expand All @@ -1588,7 +1617,7 @@ bool salvage_actor::valid_to_cut_up( const item &it ) const
if( !it.contents.empty() ) {
return false;
}
if( it.volume() < minimal_volume_to_cut ) {
if( it.weight() < minimal_weight_to_cut( it ) ) {
return false;
}

Expand Down Expand Up @@ -1622,7 +1651,7 @@ bool salvage_actor::try_to_cut_up( player &p, item &it ) const
add_msg( m_info, _( "Please empty the %s before cutting it up." ), it.tname() );
return false;
}
if( it.volume() < minimal_volume_to_cut ) {
if( it.weight() < minimal_weight_to_cut( it ) ) {
add_msg( m_info, _( "The %s is too small to salvage material from." ), it.tname() );
return false;
}
Expand All @@ -1649,9 +1678,8 @@ bool salvage_actor::try_to_cut_up( player &p, item &it ) const
int salvage_actor::cut_up( player &p, item &it, item_location &cut ) const
{
const bool filthy = cut.get_item()->is_filthy();
// total number of raw components == total volume of item.
// This can go awry if there is a volume / recipe mismatch.
int count = cut.get_item()->volume() / minimal_volume_to_cut;
// This is the value that tracks progress, as we cut pieces off, we reduce this number.
units::mass remaining_weight = cut.get_item()->weight();
// Chance of us losing a material component to entropy.
/** @EFFECT_FABRICATION reduces chance of losing components when cutting items up */
int entropy_threshold = std::max( 5, 10 - p.get_skill_level( skill_fabrication ) );
Expand All @@ -1671,39 +1699,49 @@ int salvage_actor::cut_up( player &p, item &it, item_location &cut ) const
return 0;
}

// Time based on number of components.
p.moves -= moves_per_part * count;
// Not much practice, and you won't get very far ripping things up.
p.practice( skill_fabrication, rng( 0, 5 ), 1 );

// Higher fabrication, less chance of entropy, but still a chance.
if( rng( 1, 10 ) <= entropy_threshold ) {
count -= 1;
remaining_weight *= 0.99;
}
// Fail dex roll, potentially lose more parts.
/** @EFFECT_DEX randomly reduces component loss when cutting items up */
if( dice( 3, 4 ) > p.dex_cur ) {
count -= rng( 0, 2 );
remaining_weight *= 0.95;
}
// If more than 1 material component can still be salvaged,
// chance of losing more components if the item is damaged.
// If the item being cut is not damaged, no additional losses will be incurred.
if( count > 0 && cut.get_item()->damage() > 0 ) {
if( cut.get_item()->damage() > 0 ) {
float component_success_chance = std::min( std::pow( 0.8, cut.get_item()->damage_level( 4 ) ),
1.0 );
for( int i = count; i > 0; i-- ) {
if( component_success_chance < rng_float( 0, 1 ) ) {
count--;
}
}
remaining_weight *= component_success_chance;
}

// Decided to split components evenly. Since salvage will likely change
// soon after I write this, I'll go with the one that is cleaner.
// Essentially we round-robbin through the components subtracting mass as we go.
std::map<units::mass, itype_id> weight_to_item_map;
for( const material_id &material : cut_material_components ) {
if( const auto id = material->salvaged_into() ) {
materials_salvaged[*id] = std::max( 0, count / static_cast<int>( cut_material_components.size() ) );
if( const std::optional<itype_id> id = material->salvaged_into() ) {
materials_salvaged[*id] = 0;
weight_to_item_map[ item( *id, calendar::turn_zero, item::solitary_tag{} ).weight() ] = *id;
}
}
while( remaining_weight > 0_gram && !weight_to_item_map.empty() ) {
units::mass components_weight = std::accumulate( weight_to_item_map.begin(),
weight_to_item_map.end(), 0_gram, []( const units::mass & a,
const std::pair<units::mass, itype_id> &b ) {
return a + b.first;
} );
if( components_weight > 0_gram && components_weight <= remaining_weight ) {
int count = remaining_weight / components_weight;
for( std::pair<units::mass, itype_id> mat_pair : weight_to_item_map ) {
materials_salvaged[mat_pair.second] += count;
}
remaining_weight -= components_weight * count;
}
weight_to_item_map.erase( std::prev( weight_to_item_map.end() ) );
}

add_msg( m_info, _( "You try to salvage materials from the %s." ),
Expand All @@ -1725,6 +1763,8 @@ int salvage_actor::cut_up( player &p, item &it, item_location &cut ) const
int amount = salvaged.second;
item result( mat_name, calendar::turn );
if( amount > 0 ) {
// Time based on number of components.
p.moves -= moves_per_part;
add_msg( m_good, vgettext( "Salvaged %1$i %2$s.", "Salvaged %1$i %2$s.", amount ),
amount, result.display_name( amount ) );
if( filthy ) {
Expand Down
95 changes: 95 additions & 0 deletions tests/iuse_actor_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@
#include "game.h"
#include "inventory.h"
#include "item.h"
#include "item_factory.h"
#include "itype.h"
#include "iuse_actor.h"
#include "map.h"
#include "map_selector.h"
#include "material.h"
#include "monster.h"
#include "mtype.h"
#include "player.h"
Expand Down Expand Up @@ -59,3 +65,92 @@ TEST_CASE( "manhack", "[iuse_actor][manhack]" )
g->clear_zombies();
}


static void cut_up_yields( const std::string &target )
{
map &here = get_map();
player &guy = get_avatar();
clear_avatar();
// Nominal dex to avoid yield penalty.
guy.dex_cur = 12;
//guy.set_skill_level( skill_id( "fabrication" ), 10 );
here.i_at( guy.pos() ).clear();

CAPTURE( target );
salvage_actor test_actor;
item cut_up_target{ target };
item tool{ "knife_butcher" };
const std::vector<material_id> &target_materials = cut_up_target.made_of();
units::mass smallest_yield_mass = units::mass_max;
for( const material_id &mater : target_materials ) {
if( const std::optional<itype_id> item_id = mater->salvaged_into() ) {
smallest_yield_mass = std::min( smallest_yield_mass, item_id->obj().weight );
}
}
REQUIRE( smallest_yield_mass != units::mass_max );

units::mass cut_up_target_mass = cut_up_target.weight();
item &spawned_item = here.add_item_or_charges( guy.pos(), cut_up_target );
item_location item_loc( map_cursor( guy.pos() ), &spawned_item );

REQUIRE( smallest_yield_mass <= cut_up_target_mass );

test_actor.cut_up( guy, tool, item_loc );

map_stack salvaged_items = here.i_at( guy.pos() );
units::mass salvaged_mass = 0_gram;
for( const item &salvage : salvaged_items ) {
salvaged_mass += salvage.weight();
}
CHECK( salvaged_mass <= cut_up_target_mass );
CHECK( salvaged_mass >= ( cut_up_target_mass * 0.99 ) - smallest_yield_mass );
}

TEST_CASE( "cut_up_yields" )
{
cut_up_yields( "blanket" );
cut_up_yields( "backpack_hiking" );
cut_up_yields( "boxpack" );
cut_up_yields( "case_violin" );
cut_up_yields( "down_mattress" );
cut_up_yields( "plastic_boat_hull" );
cut_up_yields( "bunker_coat" );
cut_up_yields( "bunker_pants" );
cut_up_yields( "kevlar" );
cut_up_yields( "touring_suit" );
cut_up_yields( "dress_wedding" );
cut_up_yields( "wetsuit" );
cut_up_yields( "wetsuit_booties" );
cut_up_yields( "wetsuit_hood" );
cut_up_yields( "wetsuit_spring" );
cut_up_yields( "peacoat" );
cut_up_yields( "log" );
cut_up_yields( "stick" );
cut_up_yields( "stick_long" );
cut_up_yields( "tazer" );
cut_up_yields( "control_laptop" );
cut_up_yields( "voltmeter" );
cut_up_yields( "eink_tablet_pc" );
cut_up_yields( "camera" );
cut_up_yields( "cell_phone" );
cut_up_yields( "laptop" );
cut_up_yields( "radio" );
cut_up_yields( "under_armor" );
cut_up_yields( "acidchitin_armor_small_quadruped" );
cut_up_yields( "chitin_armor_small_quadruped" );
cut_up_yields( "leather_armor_small_quadruped" );
cut_up_yields( "leatherbone_armor_small_quadruped" );
cut_up_yields( "kevlar_armor_small_quadruped" );
cut_up_yields( "rubber_armor_small_quadruped" );
}

TEST_CASE( "cut_up_yields_all", "[.]" )
{
int i = 0;
for( const itype *itp : item_controller->all() ) {
i++;
SECTION( string_format( "Item number %d", i ) ) {
cut_up_yields( itp->get_id().str() );
}
}
}

0 comments on commit 5afebd6

Please sign in to comment.