diff --git a/src/rng.cpp b/src/rng.cpp index 93a47e75819bf..ac148072ea719 100644 --- a/src/rng.cpp +++ b/src/rng.cpp @@ -113,6 +113,24 @@ int djb2_hash( const unsigned char *input ) return hash; } +std::vector rng_sequence( size_t count, int lo, int hi, int seed ) +{ + if( lo > hi ) { + std::swap( lo, hi ); + } + std::vector result; + result.reserve( count ); + + // NOLINTNEXTLINE(cata-determinism) + cata_default_random_engine eng( seed ); + std::uniform_int_distribution rng_int_dist; + const std::uniform_int_distribution::param_type param( lo, hi ); + for( size_t i = 0; i < count; i++ ) { + result.push_back( rng_int_dist( eng, param ) ); + } + return result; +} + double rng_normal( double lo, double hi ) { if( lo > hi ) { diff --git a/src/rng.h b/src/rng.h index 7683c666885d3..3f0c5de190b39 100644 --- a/src/rng.h +++ b/src/rng.h @@ -55,6 +55,15 @@ inline int roll_remainder( float value ) int djb2_hash( const unsigned char *input ); +// Generates a deterministic sequence of uniform ints. +// Note that this doesn't use or modify the global rng state but uses the seed given as parameter. +// @param count length of sequence to generate +// @param lo minimum value in sequence +// @param hi maximum value in sequence +// @param seed seed to use +// @returns deterministic vector of uniform ints +std::vector rng_sequence( size_t count, int lo, int hi, int seed = 42 ); + double rng_normal( double lo, double hi ); inline double rng_normal( double hi ) diff --git a/src/vehicle.cpp b/src/vehicle.cpp index beee2d3d501ee..44c507dafc8c7 100644 --- a/src/vehicle.cpp +++ b/src/vehicle.cpp @@ -7119,10 +7119,21 @@ item vehicle::get_folded_item() const } catch( const JsonError &e ) { debugmsg( "Error storing vehicle: %s", e.c_str() ); } - const units::volume folded_volume = std::accumulate( parts.cbegin(), parts.cend(), 0_ml, - []( const units::volume v, const vehicle_part & vp ) { - return v + ( vp.removed ? 0_ml : vp.info().folded_volume ); - } ); + + units::volume folded_volume = 0_ml; + double sum_of_damage = 0; + int num_of_parts = 0; + for( const vehicle_part &vp : parts ) { + if( vp.removed ) { + continue; + } + folded_volume += vp.info().folded_volume; + sum_of_damage += vp.damage_percent(); + num_of_parts++; + } + + // snapshot average damage of parts into both item's hp and item variable + const int avg_part_damage = static_cast( sum_of_damage / num_of_parts * folded.max_damage() ); folded.set_var( "tracking", tracking_on ? 1 : 0 ); folded.set_var( "weight", to_milligram( total_mass() ) ); @@ -7130,6 +7141,8 @@ item vehicle::get_folded_item() const folded.set_var( "name", string_format( _( "folded %s" ), name ) ); folded.set_var( "vehicle_name", name ); folded.set_var( "unfolding_time", to_moves( unfolding_time() ) ); + folded.set_var( "avg_part_damage", avg_part_damage ); + folded.set_damage( avg_part_damage ); // TODO: a better description? std::string desc = string_format( _( "A folded %s." ), name ) .append( "\n\n" ) @@ -7153,6 +7166,21 @@ bool vehicle::restore_folded_parts( const item &it ) debugmsg( "Error restoring folded vehicle parts: %s", e.c_str() ); return false; } + + // item should have snapshot of average part damage in item var. take difference of current + // item's damage and snapshotted damage, then randomly apply to parts in chunks to roughly match. + constexpr double damage_chunk = 0.25; + const double damage_diff = it.damage() - static_cast( it.get_var( "avg_part_damage", 0.0 ) ); + const int count = damage_diff / it.max_damage() * real_parts().size() / damage_chunk; + const int seed = static_cast( damage_diff ); + for( int part_idx : rng_sequence( count, 0, parts.size() - 1, seed ) ) { + vehicle_part &pt = parts[part_idx]; + if( pt.removed || pt.is_fake ) { + continue; + } + pt.base.mod_damage( damage_chunk * pt.base.max_damage() ); + } + refresh(); face.init( 0_degrees ); turn_dir = 0_degrees; diff --git a/tests/ranged_balance_test.cpp b/tests/ranged_balance_test.cpp index 5b8e8674c0adb..fb3d2a555c3a5 100644 --- a/tests/ranged_balance_test.cpp +++ b/tests/ranged_balance_test.cpp @@ -582,9 +582,9 @@ std::map hit_distribution( const targeting_graph &graph, for( int i = 0; i < iters; ++i ) { typename std::map::iterator it; if( guess ) { - it = hits.emplace( graph.select( 0.0, 1.0, *guess ), 0 ).first; + it = hits.emplace( graph.select( 0.0, 1.0, *guess ), 0.f ).first; } else { - it = hits.emplace( graph.select( 0.0, 1.0, rng_float( 0, 1 ) ), 0 ).first; + it = hits.emplace( graph.select( 0.0, 1.0, rng_float( 0, 1 ) ), 0.f ).first; } ++it->second; } @@ -1160,7 +1160,7 @@ TEST_CASE( "Default_anatomy_body_part_hit_chances", "[targeting_graph][anatomy][ const int total_hits = 1000000; for( int i = 0; i < total_hits; ++i ) { auto it = hits.emplace( tested->select_body_part_projectile_attack( 0, 1, rng_float( 0, 1 ) ), - 0 ).first; + 0.f ).first; ++it->second; } diff --git a/tests/vehicle_test.cpp b/tests/vehicle_test.cpp index e54b2e6a2db6e..faac5b4439e13 100644 --- a/tests/vehicle_test.cpp +++ b/tests/vehicle_test.cpp @@ -274,6 +274,129 @@ TEST_CASE( "Unfolding vehicle parts and testing degradation", "[item][degradatio clear_vehicles( &get_map() ); } +struct folded_item_damage_preset { + itype_id folded_vehicle_item; + int item_damage_first_fold; + int item_damage_second_fold; + int part_damage_second_unfold; // sum of damage over all parts + int part_damage_third_unfold; // sum of damage over all parts +}; + +static void check_folded_item_to_parts_damage_transfer( const folded_item_damage_preset &preset ) +{ + CAPTURE( preset.folded_vehicle_item.str(), + preset.item_damage_first_fold, preset.item_damage_second_fold, + preset.part_damage_second_unfold, preset.part_damage_third_unfold ); + + // exact damage numbers are checked against, there should be almost no rng, + // only the part damage is pseudo-random spread, while total damage should + // round trip well in integers + clear_avatar(); + clear_map(); + + map &m = get_map(); + Character &u = get_player_character(); + + u.worn.wear_item( u, item( "debug_backpack" ), false, false ); + + item veh_item( preset.folded_vehicle_item ); + + // unfold fresh item factory item + complete_activity( u, vehicle_unfolding_activity_actor( veh_item ) ); + + optional_vpart_position ovp = m.veh_at( u.get_location() ); + REQUIRE( ovp.has_value() ); + + // don't actually need point_north but damage_all filters out direct damage + // do some damage so it is transferred when folding + ovp->vehicle().damage_all( 100, 100, damage_type::PURE, ovp->mount() + point_north ); + + // fold vehicle into an item + complete_activity( u, vehicle_folding_activity_actor( ovp->vehicle() ) ); + + ovp = m.veh_at( u.get_location() ); + REQUIRE( !ovp.has_value() ); + + // copy the player-folded vehicle item and delete it from the map + map_stack map_items = m.i_at( u.pos_bub() ); + REQUIRE( map_items.size() == 1 ); + item player_folded_veh = map_items.only_item(); + map_items.clear(); + + // check the damage was transferred from parts to folded item + CHECK( player_folded_veh.damage() == preset.item_damage_first_fold ); + CHECK( player_folded_veh.get_var( "avg_part_damage", 0.0 ) == preset.item_damage_first_fold ); + + complete_activity( u, vehicle_unfolding_activity_actor( player_folded_veh ) ); + + ovp = m.veh_at( u.get_location() ); + REQUIRE( ovp.has_value() ); + + int part_damage_before = 0; + for( const vpart_reference &vpr : ovp->vehicle().get_all_parts() ) { + part_damage_before += vpr.part().damage(); + } + + // check damage correctly transferred from item to vehicle parts + CHECK( part_damage_before == preset.part_damage_second_unfold ); + + complete_activity( u, vehicle_folding_activity_actor( ovp->vehicle() ) ); + + ovp = m.veh_at( u.get_location() ); + REQUIRE( !ovp.has_value() ); + map_items = m.i_at( u.pos_bub() ); + REQUIRE( map_items.size() == 1 ); + player_folded_veh = map_items.only_item(); + map_items.clear(); + + // check that we don't add extra item damage after folding + CHECK( player_folded_veh.damage() == preset.item_damage_first_fold ); + CHECK( player_folded_veh.get_var( "avg_part_damage", 0.0 ) == preset.item_damage_first_fold ); + + // add some more damage to the item + player_folded_veh.mod_damage( 300 ); + + // unfold and check extra damage gets distributed into vehicleparts + complete_activity( u, vehicle_unfolding_activity_actor( player_folded_veh ) ); + ovp = m.veh_at( u.get_location() ); + REQUIRE( ovp.has_value() ); + + // add up damage on all parts + int part_damage_after = 0; + for( const vpart_reference &vpr : ovp->vehicle().get_all_parts() ) { + part_damage_after += vpr.part().damage(); + } + + { + INFO( "Checking extra item damage gets distributed to vehicle parts." ); + CHECK( part_damage_after > part_damage_before ); + CHECK( part_damage_after == preset.part_damage_third_unfold ); + } + + complete_activity( u, vehicle_folding_activity_actor( ovp->vehicle() ) ); + + REQUIRE( !m.veh_at( u.get_location() ) ); + map_items = m.i_at( u.pos_bub() ); + REQUIRE( map_items.size() == 1 ); + player_folded_veh = map_items.only_item(); + map_items.clear(); + + CHECK( player_folded_veh.damage() == preset.item_damage_second_fold ); + CHECK( player_folded_veh.get_var( "avg_part_damage", 0.0 ) == preset.item_damage_second_fold ); +} + +TEST_CASE( "Check folded item damage transfers to parts and vice versa", "[item][vehicle]" ) +{ + std::vector presets { + { itype_folded_wheelchair_generic, 2111, 2277, 12666, 13666 }, + { itype_folded_bicycle, 1689, 1961, 18582, 21582 }, + }; + + for( const folded_item_damage_preset &preset : presets ) { + check_folded_item_to_parts_damage_transfer( preset ); + } +} + // Basically a copy of vehicle::connect() that uses an arbitrary cord type static void connect_power_line( const tripoint &src_pos, const tripoint &dst_pos, const itype_id &itm )