From f23654e06d5af3ce25f5feab2bcb1b092d648d97 Mon Sep 17 00:00:00 2001 From: boyboytemp Date: Sun, 15 Dec 2024 20:32:44 +0300 Subject: [PATCH] fix displaying of stats for ablative armor Currently protection/coverage stats are not displayed in 'sort armor screen' and 'body status screen'. The issue was reported originally in #62126 --- src/armor_layers.cpp | 2 +- src/item.cpp | 196 +++++++++++++++++++++++++++++++++------- src/item.h | 10 +- tests/coverage_test.cpp | 41 +++++++++ tests/iteminfo_test.cpp | 79 ++++++++++++++++ 5 files changed, 289 insertions(+), 39 deletions(-) diff --git a/src/armor_layers.cpp b/src/armor_layers.cpp index 1f901aa14a124..490612760ce9d 100644 --- a/src/armor_layers.cpp +++ b/src/armor_layers.cpp @@ -293,7 +293,7 @@ std::vector clothing_properties( add_folded_name_and_value( props, _( "Encumbrance:" ), string_format( "%3d", encumbrance ), width ); add_folded_name_and_value( props, _( "Warmth:" ), string_format( "%3d", - worn_item.get_warmth() ), width ); + worn_item.get_warmth( used_bp ) ), width ); return props; } diff --git a/src/item.cpp b/src/item.cpp index 4643a6d0901d1..c08048657543a 100644 --- a/src/item.cpp +++ b/src/item.cpp @@ -955,7 +955,7 @@ bool item::is_frozen_liquid() const return made_of( phase_id::SOLID ) && made_of_from_type( phase_id::LIQUID ); } -bool item::covers( const sub_bodypart_id &bp ) const +bool item::covers( const sub_bodypart_id &bp, bool check_ablative_armor ) const { // if the item has no armor data it doesn't cover that part const islot_armor *armor = find_armor_data(); @@ -982,18 +982,18 @@ bool item::covers( const sub_bodypart_id &bp ) const iterate_covered_sub_body_parts_internal( get_side(), [&]( const sub_bodypart_str_id & covered ) { does_cover = does_cover || bp == covered; - } ); + }, check_ablative_armor ); return does_cover || subpart_cover; } -bool item::covers( const bodypart_id &bp ) const +bool item::covers( const bodypart_id &bp, bool check_ablative_armor ) const { bool does_cover = false; bool subpart_cover = false; iterate_covered_body_parts_internal( get_side(), [&]( const bodypart_str_id & covered ) { does_cover = does_cover || bp == covered; - } ); + }, check_ablative_armor ); return does_cover || subpart_cover; } @@ -1111,12 +1111,14 @@ static void iterate_helper_sbp( const item *i, const side s, } void item::iterate_covered_sub_body_parts_internal( const side s, - const std::function &cb ) const + const std::function &cb, + bool check_ablative_armor ) const + { iterate_helper_sbp( this, s, cb ); //check for ablative armor too - if( is_ablative() ) { + if( check_ablative_armor and is_ablative() ) { for( const item_pocket *pocket : get_all_ablative_pockets() ) { if( !pocket->empty() ) { // get the contained plate @@ -1159,12 +1161,13 @@ static void iterate_helper( const item *i, const side s, } void item::iterate_covered_body_parts_internal( const side s, - const std::function &cb ) const + const std::function &cb, + bool check_ablative_armor ) const { iterate_helper( this, s, cb ); //check for ablative armor too - if( is_ablative() ) { + if( check_ablative_armor and is_ablative() ) { for( const item_pocket *pocket : get_all_ablative_pockets() ) { if( !pocket->empty() ) { // get the contained plate @@ -8411,36 +8414,75 @@ int item::get_avg_coverage( const cover_type &type ) const int item::get_coverage( const bodypart_id &bodypart, const cover_type &type ) const { + int coverage = 0; if( const armor_portion_data *portion_data = portion_for_bodypart( bodypart ) ) { switch( type ) { - case cover_type::COVER_DEFAULT: - return portion_data->coverage; - case cover_type::COVER_MELEE: - return portion_data->cover_melee; - case cover_type::COVER_RANGED: - return portion_data->cover_ranged; - case cover_type::COVER_VITALS: - return portion_data->cover_vitals; + case cover_type::COVER_DEFAULT: { + coverage = portion_data->coverage; + break; + } + case cover_type::COVER_MELEE: { + coverage = portion_data->cover_melee; + break; + } + case cover_type::COVER_RANGED: { + coverage = portion_data->cover_ranged; + break; + } + case cover_type::COVER_VITALS: { + coverage = portion_data->cover_vitals; + break; + } } } - return 0; + if( is_ablative() ) { + for( const item_pocket *pocket : contents.get_all_ablative_pockets() ) { + if( !pocket->empty() ) { + // get the contained plate + const item &ablative_armor = pocket->front(); + //Rationale for this kind of aggregation is that ablative armor + //can't be bigger than an armor that contains it. But it may cover + //new body parts, e.g. face shield attached to hard hat. + coverage = std::max( coverage, ablative_armor.get_coverage( bodypart ) ); + } + } + } + return coverage; } int item::get_coverage( const sub_bodypart_id &bodypart, const cover_type &type ) const { + int coverage = 0; if( const armor_portion_data *portion_data = portion_for_bodypart( bodypart ) ) { switch( type ) { - case cover_type::COVER_DEFAULT: - return portion_data->coverage; - case cover_type::COVER_MELEE: - return portion_data->cover_melee; - case cover_type::COVER_RANGED: - return portion_data->cover_ranged; - case cover_type::COVER_VITALS: - return portion_data->cover_vitals; + case cover_type::COVER_DEFAULT: { + coverage = portion_data->coverage; + break; + } + case cover_type::COVER_MELEE: { + coverage = portion_data->cover_melee; + break; + } + case cover_type::COVER_RANGED: { + coverage = portion_data->cover_ranged; + break; + } + case cover_type::COVER_VITALS: { + coverage = portion_data->cover_vitals; + break; + } } } - return 0; + if( is_ablative() ) { + for( const item_pocket *pocket : contents.get_all_ablative_pockets() ) { + if( !pocket->empty() ) { + // get the contained plate + const item &ablative_armor = pocket->front(); + coverage = std::max( coverage, ablative_armor.get_coverage( bodypart ) ); + } + } + } + return coverage; } bool item::has_sublocations() const @@ -8501,6 +8543,7 @@ float item::get_thickness( const bodypart_id &bp ) const if( t == nullptr ) { return is_pet_armor() ? type->pet_armor->thickness : 0.0f; } + float avg_thickness = 0.0f; for( const armor_portion_data &data : t->data ) { if( !data.covers.has_value() ) { @@ -8508,12 +8551,33 @@ float item::get_thickness( const bodypart_id &bp ) const } for( const bodypart_str_id &bpid : data.covers.value() ) { if( bp == bpid ) { - return data.avg_thickness; + avg_thickness = data.avg_thickness; + break; } } + if( avg_thickness > 0.0f ) { + break; + } } - // body part not covered by this armour - return 0.0f; + if( is_ablative() ) { + int ablatives = 0; + float ablative_thickness = 0.0; + for( const item_pocket *pocket : contents.get_all_ablative_pockets() ) { + if( !pocket->empty() ) { + // get the contained plate + const item &ablative_armor = pocket->front(); + float tmp_thickness = ablative_armor.get_thickness( bp ); + if( tmp_thickness > 0.0f ) { + ablative_thickness += tmp_thickness; + ablatives += 1; + } + } + } + if( ablatives ) { + avg_thickness += ablative_thickness / ablatives; + } + } + return avg_thickness; } float item::get_thickness( const sub_bodypart_id &bp ) const @@ -8538,7 +8602,8 @@ int item::get_warmth( const bodypart_id &bp ) const { double warmth_val = 0.0; float limb_coverage = 0.0f; - if( !covers( bp ) ) { + bool check_ablative_armor = false; + if( !covers( bp, check_ablative_armor ) ) { return 0; } warmth_val = get_warmth(); @@ -8551,7 +8616,7 @@ int item::get_warmth( const bodypart_id &bp ) const limb_coverage = 100; } else { for( const sub_bodypart_str_id &sbp : bp->sub_parts ) { - if( !covers( sbp ) ) { + if( !covers( sbp, check_ablative_armor ) ) { continue; } @@ -8559,7 +8624,19 @@ int item::get_warmth( const bodypart_id &bp ) const limb_coverage += sbp->max_coverage; } } - return std::round( warmth_val * limb_coverage / 100.0f ); + int warmth = std::round( warmth_val * limb_coverage / 100.0f ); + + if( is_ablative() ) { + for( const item_pocket *pocket : contents.get_all_ablative_pockets() ) { + if( !pocket->empty() ) { + // get the contained plate + const item &ablative_armor = pocket->front(); + warmth += ablative_armor.get_warmth( bp ); + } + } + } + + return warmth; } units::volume item::get_pet_armor_max_vol() const @@ -8927,6 +9004,19 @@ int item::breathability( const bodypart_id &bp ) const } const armor_portion_data *a = portion_for_bodypart( bp ); if( a == nullptr ) { + //This body part might still be covered by attachments of this armor (for example face shield). + //Check their breathability. + if( is_ablative() ) { + for( const item_pocket *pocket : contents.get_all_ablative_pockets() ) { + if( !pocket->empty() ) { + // get the contained plate + const item &ablative_armor = pocket->front(); + if( ablative_armor.covers( bp ) ) { + return ablative_armor.breathability( bp ); + } + } + } + } // if it doesn't cover it breathes great return 100; } @@ -9289,7 +9379,28 @@ std::vector item::armor_made_of( const bodypart_id &bp ) for( const part_material &m : d.materials ) { matlist.emplace_back( &m ); } - return matlist; + break; + } + if( !matlist.empty() ) { + break; + } + } + if( is_ablative() ) { + for( const item_pocket *pocket : contents.get_all_ablative_pockets() ) { + if( !pocket->empty() ) { + // get the contained plate + const item &ablative_armor = pocket->front(); + auto abl_mat = ablative_armor.armor_made_of( bp ); + //It is not clear how to aggregate armor material from different pockets. + //A vest might contain two different plates from different materials. + //But only one plate can be hit at one attack (according to Character::ablative_armor_absorb). + //So for now return here only material for one plate + base clothing materials. + if( !abl_mat.empty() ) { + matlist.insert( std::end( matlist ), std::begin( abl_mat ), std::end( abl_mat ) ); + return matlist; + } + + } } } return matlist; @@ -9313,7 +9424,24 @@ std::vector item::armor_made_of( const sub_bodypart_id &b for( const part_material &m : d.materials ) { matlist.emplace_back( &m ); } - return matlist; + break; + } + if( !matlist.empty() ) { + break; + } + } + if( is_ablative() ) { + for( const item_pocket *pocket : contents.get_all_ablative_pockets() ) { + if( !pocket->empty() ) { + // get the contained plate + const item &ablative_armor = pocket->front(); + auto abl_mat = ablative_armor.armor_made_of( bp ); + if( !abl_mat.empty() ) { + matlist.insert( std::end( matlist ), std::begin( abl_mat ), std::end( abl_mat ) ); + return matlist; + } + + } } } return matlist; diff --git a/src/item.h b/src/item.h index 461fd00251b50..e3ea202ce2ccc 100644 --- a/src/item.h +++ b/src/item.h @@ -2111,11 +2111,11 @@ class item : public visitable /** * Whether this item (when worn) covers the given body part. */ - bool covers( const bodypart_id &bp ) const; + bool covers( const bodypart_id &bp, bool check_ablative_armor = true ) const; /** * Whether this item (when worn) covers the given sub body part. */ - bool covers( const sub_bodypart_id &bp ) const; + bool covers( const sub_bodypart_id &bp, bool check_ablative_armor = true ) const; // do both items overlap a bodypart at all? returns the side that conflicts via rhs std::optional covers_overlaps( const item &rhs ) const; /** @@ -3056,9 +3056,11 @@ class item : public visitable bool process_internal( map &here, Character *carrier, const tripoint_bub_ms &pos, float insulation, temperature_flag flag, float spoil_modifier, bool watertight_container ); void iterate_covered_body_parts_internal( side s, - const std::function &cb ) const; + const std::function &cb, + bool check_ablative_armor = true ) const; void iterate_covered_sub_body_parts_internal( side s, - const std::function &cb ) const; + const std::function &cb, + bool check_ablative_armor = true ) const; /** * Calculate the thermal energy and temperature change of the item * @param temp Temperature of surroundings diff --git a/tests/coverage_test.cpp b/tests/coverage_test.cpp index 86c162fd31bd5..53498714fe122 100644 --- a/tests/coverage_test.cpp +++ b/tests/coverage_test.cpp @@ -27,6 +27,8 @@ static constexpr tripoint_bub_ms dude_pos( HALF_MAPSIZE_X + 4, HALF_MAPSIZE_Y, 0 static constexpr tripoint_bub_ms mon_pos( HALF_MAPSIZE_X + 3, HALF_MAPSIZE_Y, 0 ); static constexpr tripoint_bub_ms badguy_pos( HALF_MAPSIZE_X + 1, HALF_MAPSIZE_Y, 0 ); +static const sub_bodypart_str_id sub_body_part_coverage_test_eye_r( "eyes_right" ); + static void check_near( const std::string &subject, float actual, const float expected, const float tolerance ) { @@ -237,6 +239,45 @@ TEST_CASE( "Ghost_ablative_vest", "[coverage]" ) } } +TEST_CASE( "helmet_with_face_shield_coverage", "[coverage]" ) +{ + Character &dummy = get_player_character(); + clear_avatar(); + + item hat_hard( "hat_hard" ); + CHECK( hat_hard.get_coverage( body_part_eyes ) == 0 ); + + WHEN( "wearing helmet with face shield should cover eyes and mouth" ) { + item face_shield( "face_shield" ); + REQUIRE( hat_hard.put_in( face_shield, pocket_type::CONTAINER ).success() ); + dummy.wear_item( hat_hard ); + + CHECK( hat_hard.get_coverage( body_part_eyes ) == 100 ); + CHECK( hat_hard.get_coverage( body_part_mouth ) == 100 ); + CHECK( hat_hard.get_coverage( sub_body_part_coverage_test_eye_r ) == 100 ); + } + +} + +TEST_CASE( "vest_with_plate_coverage", "[coverage]" ) +{ + //Vest covers torso_upper and torso_lower + item vest = item( "ballistic_vest_esapi" ); + //100 (torso_upper coverage) * 0.6 (torso_upper max_coverage) + 80 (torso_lower coverage) * 0.4 + CHECK( vest.get_coverage( body_part_torso ) == 92 ); + + WHEN( "inserting 2 plates" ) { + //Each plate covers torso_upper with coverage 45 + CHECK( vest.put_in( item( "test_plate" ), pocket_type::CONTAINER ).success() ); + CHECK( vest.put_in( item( "test_plate" ), pocket_type::CONTAINER ).success() ); + + THEN( "vest with plates should retain the same coverage" ) { + CHECK( vest.get_coverage( body_part_torso ) == 92 ); + } + } + +} + TEST_CASE( "Off_Limb_Ghost_ablative_vest", "[coverage]" ) { SECTION( "Ablative not covered seperate limb" ) { diff --git a/tests/iteminfo_test.cpp b/tests/iteminfo_test.cpp index dd069b6d32d5c..2cc31c05519a1 100644 --- a/tests/iteminfo_test.cpp +++ b/tests/iteminfo_test.cpp @@ -52,6 +52,8 @@ static const trait_id trait_WOOLALLERGY( "WOOLALLERGY" ); static const vitamin_id vitamin_human_flesh_vitamin( "human_flesh_vitamin" ); +static const sub_bodypart_str_id sub_body_part_iteminfo_test_eye_r( "eyes_right" ); + // ITEM INFO // ========= // @@ -1235,6 +1237,83 @@ TEST_CASE( "armor_stats", "[armor][protection]" ) expected_armor_values( item( itype_zentai ), 0.1f, 0.1f, 0.08f, 0.1f ); expected_armor_values( item( itype_tshirt ), 0.1f, 0.1f, 0.08f, 0.1f ); expected_armor_values( item( itype_dress_shirt ), 0.1f, 0.1f, 0.08f, 0.1f ); + +} + + +TEST_CASE( "helmet_with_pockets_stats", "[iteminfo][armor][protection]" ) +{ + bodypart_id bp_head = body_part_head.id(); + bodypart_id bp_eyes = body_part_eyes.id(); + sub_bodypart_id eye_r = sub_body_part_iteminfo_test_eye_r.id(); + + item hh( "hat_hard" ); + THEN( "base stats" ) { + //resistance stats + CHECK( hh.resist( STATIC( damage_type_id( "bash" ) ), false, bp_head ) == Approx( 8.f ) ); + CHECK( hh.resist( STATIC( damage_type_id( "bash" ) ), false, bp_eyes ) == Approx( 0.f ) ); + CHECK( hh.resist( STATIC( damage_type_id( "bash" ) ), false, eye_r ) == Approx( 0.f ) ); + //warmth stats: 5 (hat's warmth) * 0.4 (hat's body part coverage) + CHECK( hh.get_warmth( bp_head ) == 2 ); + CHECK( hh.get_warmth( bp_eyes ) == 0 ); + } + + + WHEN( "inserting face shield" ) { + item face_shield( "face_shield" ); + REQUIRE( hh.put_in( face_shield, pocket_type::CONTAINER ).success() ); + THEN( "eyes should be protected" ) { + CHECK( hh.resist( STATIC( damage_type_id( "bash" ) ), false, bp_head ) == Approx( 8.f ) ); + CHECK( hh.resist( STATIC( damage_type_id( "bash" ) ), false, bp_eyes ) == Approx( 6.f ) ); + CHECK( hh.resist( STATIC( damage_type_id( "bash" ) ), false, eye_r ) == Approx( 6.f ) ); + } + THEN( "warmth should not change" ) { + CHECK( hh.get_warmth( bp_head ) == 2 ); + CHECK( hh.get_warmth( bp_eyes ) == 0 ); + } + THEN( "breathbility should be 0" ) { + CHECK( hh.breathability( bp_eyes ) == 0 ); + } + } + WHEN( "adding nape protector to the helmet" ) { + item nape_protector( "nape_protector" ); + REQUIRE( hh.put_in( nape_protector, pocket_type::CONTAINER ).success() ); + THEN( "head's warmth is increased" ) { + CHECK( nape_protector.get_warmth( bp_head ) == 2 ); + //2 (base warmth) + 4 (nape's warmth) * 0.4 (nape's body part coverage) + CHECK( hh.get_warmth( bp_head ) == 4 ); + } + WHEN( "adding ear muffs to the helmet" ) { + item ear_muffs( "attachable_ear_muffs" ); + REQUIRE( hh.put_in( ear_muffs, pocket_type::CONTAINER ).success() ); + THEN( "head's warmth should be increased even more" ) { + CHECK( ear_muffs.get_warmth( bp_head ) == 2 ); + CHECK( hh.get_warmth( bp_head ) == 6 ); + } + } + } + +} + + +TEST_CASE( "vest_with_plate_stats", "[iteminfo][armor][protection]" ) +{ + bodypart_id bp_torso = body_part_torso.id(); + + item vest = item( "ballistic_vest_esapi" ); + //nylon: 1 (mat resist) * 1 (thickness) + //kevlar: 1.5 * 4.4 + CHECK( vest.resist( STATIC( damage_type_id( "bash" ) ), false, bp_torso ) == Approx( 7.6f ) ); + + WHEN( "inserting plate" ) { + CHECK( vest.put_in( item( "test_plate" ), pocket_type::CONTAINER ).success() ); + + THEN( "resist should be increased" ) { + //previous + 1 * 25 + CHECK( vest.resist( STATIC( damage_type_id( "bash" ) ), false, bp_torso ) == Approx( 32.6f ) ); + } + } + } // Check that a string is provided in some iteminfo