From 586e726b0f42b8645d1ebd0344120d54b4b4c0b9 Mon Sep 17 00:00:00 2001 From: bombasticSlacks Date: Fri, 10 Jun 2022 11:48:50 -0300 Subject: [PATCH 1/2] first test can make cleaner --- data/mods/TEST_DATA/items.json | 60 ++++++++++++++++++++++++++++++++++ src/character_armor.cpp | 52 ++++++++++++++--------------- src/item.cpp | 27 +++++++++++++++ tests/coverage_test.cpp | 51 +++++++++++++++++++++++++++++ 4 files changed, 163 insertions(+), 27 deletions(-) diff --git a/data/mods/TEST_DATA/items.json b/data/mods/TEST_DATA/items.json index 9699eacaf46db..cddc91ebc6a37 100644 --- a/data/mods/TEST_DATA/items.json +++ b/data/mods/TEST_DATA/items.json @@ -3553,6 +3553,66 @@ } ] }, + { + "id": "test_ghost_vest", + "type": "ARMOR", + "category": "armor", + "//": "Vest with no coverage to test if it still protects", + "name": { "str": "US ghost vest" }, + "description": "Ballistic armor with specialized pockets on the front, back and sides for armored plates. The soft armor plate carrier is still protective but it won't stop high energy projectiles.", + "weight": "2911 g", + "volume": "6 L", + "price": 160000, + "price_postapoc": 1000, + "symbol": "[", + "material": [ "nylon", "kevlar" ], + "color": "light_gray", + "warmth": 15, + "flags": [ "STURDY", "OUTER", "WATER_FRIENDLY" ], + "use_action": [ { "type": "attach_molle", "size": 6 }, { "type": "detach_molle" } ], + "pocket_data": [ + { + "pocket_type": "CONTAINER", + "ablative": true, + "volume_encumber_modifier": 0, + "max_contains_volume": "1600 ml", + "max_contains_weight": "5 kg", + "moves": 200, + "description": "Pocket for front plate", + "flag_restriction": [ "ABLATIVE_LARGE" ] + }, + { + "pocket_type": "CONTAINER", + "ablative": true, + "volume_encumber_modifier": 0, + "max_contains_volume": "1600 ml", + "max_contains_weight": "5 kg", + "moves": 200, + "description": "Pocket for back plate", + "flag_restriction": [ "ABLATIVE_LARGE" ] + }, + { + "pocket_type": "CONTAINER", + "ablative": true, + "volume_encumber_modifier": 0, + "max_contains_volume": "800 ml", + "max_contains_weight": "2 kg", + "moves": 200, + "description": "Pocket for right side plate", + "flag_restriction": [ "ABLATIVE_MEDIUM" ] + }, + { + "pocket_type": "CONTAINER", + "ablative": true, + "volume_encumber_modifier": 0, + "max_contains_volume": "800 ml", + "max_contains_weight": "2 kg", + "moves": 200, + "description": "Pocket for left side plate", + "flag_restriction": [ "ABLATIVE_MEDIUM" ] + } + ] + }, { "id": "test_plate", "type": "ARMOR", diff --git a/src/character_armor.cpp b/src/character_armor.cpp index ca006e4585466..a1fb7079904ef 100644 --- a/src/character_armor.cpp +++ b/src/character_armor.cpp @@ -356,30 +356,37 @@ bool Character::armor_absorb( damage_unit &du, item &armor, const bodypart_id &b { item::cover_type ctype = item::get_cover_type( du.type ); - if( roll > armor.get_coverage( sbp, ctype ) ) { - return false; - } - - // if the armor location has ablative armor apply that first - if( armor.is_ablative() ) { + // if we've gotten here but the item doesn't actually cover just apply ablative armor + if( armor.get_coverage( sbp, ctype ) == 0 && armor.is_ablative() ) { ablative_armor_absorb( du, armor, sbp, roll ); - } + } else { + if( roll > armor.get_coverage( sbp, ctype ) ) { + return false; + } - // if we hit the specific location then we should continue with absorption as normal + // if the armor location has ablative armor apply that first + if( armor.is_ablative() ) { + ablative_armor_absorb( du, armor, sbp, roll ); + } + // if we hit the specific location then we should continue with absorption as normal - // reduce the damage - // -1 is passed as roll so that each material is rolled individually - armor.mitigate_damage( du, sbp, -1 ); - // check if the armor was damaged - item::armor_status damaged = armor.damage_armor_durability( du, bp ); + // reduce the damage + // -1 is passed as roll so that each material is rolled individually + armor.mitigate_damage( du, sbp, -1 ); - // describe what happened if the armor took damage - if( damaged == item::armor_status::DAMAGED || damaged == item::armor_status::DESTROYED ) { - describe_damage( du, armor ); + // check if the armor was damaged + item::armor_status damaged = armor.damage_armor_durability( du, bp ); + + // describe what happened if the armor took damage + if( damaged == item::armor_status::DAMAGED || damaged == item::armor_status::DESTROYED ) { + describe_damage( du, armor ); + } + return damaged == item::armor_status::DESTROYED; } - return damaged == item::armor_status::DESTROYED; + + return false; } bool Character::armor_absorb( damage_unit &du, item &armor, const bodypart_id &bp, int roll ) @@ -415,16 +422,7 @@ bool Character::ablative_armor_absorb( damage_unit &du, item &armor, const sub_b // get the contained plate item &ablative_armor = pocket->front(); - float ablative_coverage = ablative_armor.get_coverage( bp, ctype ); - float armor_coverage = armor.get_coverage( bp, ctype ); - - // ablative armor stores its overall coverage ex: covers 30% of the torso - // but if that plate is in a vest that only covers 60% of the torso then - // it covers 50% of the vest so need to scale the coverage appropriately - // since the attack has already hit the vest now we are checking if it hits - // a plate - - float coverage = ( ablative_coverage / armor_coverage ) * 100; + float coverage = ablative_armor.get_coverage( bp, ctype ); // if the attack hits this plate if( roll < coverage ) { diff --git a/src/item.cpp b/src/item.cpp index 3ce57708ccf84..13cb609a86bb2 100644 --- a/src/item.cpp +++ b/src/item.cpp @@ -859,18 +859,45 @@ bool item::covers( const sub_bodypart_id &bp ) const } bool does_cover = false; + bool subpart_cover = false; + iterate_covered_sub_body_parts_internal( get_side(), [&]( const sub_bodypart_str_id & covered ) { does_cover = does_cover || bp == covered; } ); + + // check if a piece of ablative armor covers the location + for( const item_pocket *pocket : get_all_contained_pockets() ) { + // if the pocket is ablative and not empty we should check it + if( pocket->get_pocket_data()->ablative && !pocket->empty() ) { + // get the contained plate + const item &ablative_armor = pocket->front(); + + subpart_cover = subpart_cover || ablative_armor.covers( bp ); + } + } + return does_cover; } bool item::covers( const bodypart_id &bp ) 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 if a piece of ablative armor covers the location + for( const item_pocket *pocket : get_all_contained_pockets() ) { + // if the pocket is ablative and not empty we should check it + if( pocket->get_pocket_data()->ablative && !pocket->empty() ) { + // get the contained plate + const item &ablative_armor = pocket->front(); + + subpart_cover = subpart_cover || ablative_armor.covers( bp ); + } + } + return does_cover; } diff --git a/tests/coverage_test.cpp b/tests/coverage_test.cpp index 8dedb57a73582..2fdd4387f1041 100644 --- a/tests/coverage_test.cpp +++ b/tests/coverage_test.cpp @@ -79,6 +79,41 @@ static float get_avg_melee_dmg( std::string clothing_id, bool infect_risk = fals return static_cast( dam_acc ) / num_hits; } +static float get_avg_melee_dmg( item cloth, bool infect_risk = false ) +{ + monster zed( mon_manhack, mon_pos ); + standard_npc dude( "TestCharacter", dude_pos, {}, 0, 8, 8, 8, 8 ); + if( infect_risk ) { + cloth.set_flag( json_flag_FILTHY ); + } + int dam_acc = 0; + int num_hits = 0; + for( int i = 0; i < num_iters; i++ ) { + clear_character( dude, true ); + dude.setpos( dude_pos ); + dude.wear_item( cloth, false ); + dude.add_effect( effect_sleep, 1_hours ); + if( zed.melee_attack( dude, 10000.0f ) ) { + num_hits++; + } + cloth.set_damage( cloth.min_damage() ); + if( !infect_risk ) { + dam_acc += dude.get_hp_max() - dude.get_hp(); + } else if( dude.has_effect( effect_bite ) ) { + dam_acc++; + } + if( dude.is_dead() ) { + break; + } + } + CAPTURE( dude.is_dead() ); + const std::string ret_type = infect_risk ? "infections" : "damage total"; + INFO( string_format( "%s landed %d hits on character, causing %d %s.", zed.get_name(), num_hits, + dam_acc, ret_type ) ); + num_hits = num_hits ? num_hits : 1; + return static_cast( dam_acc ) / num_hits; +} + static float get_avg_bullet_dmg( std::string clothing_id ) { clear_map(); @@ -181,3 +216,19 @@ TEST_CASE( "Proportional armor material resistances", "[material]" ) check_not_near( "Average damage", dmg, base_line, 0.05f ); } } + +TEST_CASE( "Ghost ablative vest", "[coverage]" ) +{ + SECTION( "Ablative not covered" ) { + item full = item( "test_ghost_vest" ); + item esapi1 = item( "test_plate" ); + item esapi2 = item( "test_plate" ); + full.put_in( esapi1, item_pocket::pocket_type::CONTAINER ); + full.put_in( esapi2, item_pocket::pocket_type::CONTAINER ); + item empty = item( "test_ghost_vest" ); + const float dmg_full = get_avg_melee_dmg( full ); + const float dmg_empty = get_avg_melee_dmg( empty ); + check_near( "Average damage", dmg_full, dmg_empty, 0.2f ); + } +} + From 2edbc8e9b7ed2e156a2b6a4208bfc8f2f8c2b96f Mon Sep 17 00:00:00 2001 From: bombasticSlacks Date: Fri, 10 Jun 2022 12:03:28 -0300 Subject: [PATCH 2/2] ablative armor can now cover more locations than the base --- data/mods/TEST_DATA/items.json | 3 ++- src/character_armor.cpp | 41 +++++++++++++--------------------- src/item.cpp | 4 ++-- tests/coverage_test.cpp | 13 ++++++----- 4 files changed, 28 insertions(+), 33 deletions(-) diff --git a/data/mods/TEST_DATA/items.json b/data/mods/TEST_DATA/items.json index cddc91ebc6a37..aae5ba584d228 100644 --- a/data/mods/TEST_DATA/items.json +++ b/data/mods/TEST_DATA/items.json @@ -3611,7 +3611,8 @@ "description": "Pocket for left side plate", "flag_restriction": [ "ABLATIVE_MEDIUM" ] } - ] + ], + "armor": [ { "encumbrance": 2, "coverage": 100, "covers": [ "torso" ], "specifically_covers": [ "torso_lower" ] } ] }, { "id": "test_plate", diff --git a/src/character_armor.cpp b/src/character_armor.cpp index a1fb7079904ef..880f003a1bea2 100644 --- a/src/character_armor.cpp +++ b/src/character_armor.cpp @@ -356,37 +356,28 @@ bool Character::armor_absorb( damage_unit &du, item &armor, const bodypart_id &b { item::cover_type ctype = item::get_cover_type( du.type ); - // if we've gotten here but the item doesn't actually cover just apply ablative armor - if( armor.get_coverage( sbp, ctype ) == 0 && armor.is_ablative() ) { + // if the armor location has ablative armor apply that first + if( armor.is_ablative() ) { ablative_armor_absorb( du, armor, sbp, roll ); - } else { - if( roll > armor.get_coverage( sbp, ctype ) ) { - return false; - } - - // if the armor location has ablative armor apply that first - if( armor.is_ablative() ) { - ablative_armor_absorb( du, armor, sbp, roll ); - } - - // if we hit the specific location then we should continue with absorption as normal + } + // if the core armor is missed then exit + if( roll > armor.get_coverage( sbp, ctype ) ) { + return false; + } - // reduce the damage - // -1 is passed as roll so that each material is rolled individually - armor.mitigate_damage( du, sbp, -1 ); + // reduce the damage + // -1 is passed as roll so that each material is rolled individually + armor.mitigate_damage( du, sbp, -1 ); - // check if the armor was damaged - item::armor_status damaged = armor.damage_armor_durability( du, bp ); + // check if the armor was damaged + item::armor_status damaged = armor.damage_armor_durability( du, bp ); - // describe what happened if the armor took damage - if( damaged == item::armor_status::DAMAGED || damaged == item::armor_status::DESTROYED ) { - describe_damage( du, armor ); - } - return damaged == item::armor_status::DESTROYED; + // describe what happened if the armor took damage + if( damaged == item::armor_status::DAMAGED || damaged == item::armor_status::DESTROYED ) { + describe_damage( du, armor ); } - - return false; + return damaged == item::armor_status::DESTROYED; } bool Character::armor_absorb( damage_unit &du, item &armor, const bodypart_id &bp, int roll ) diff --git a/src/item.cpp b/src/item.cpp index 13cb609a86bb2..098f8f8971896 100644 --- a/src/item.cpp +++ b/src/item.cpp @@ -876,7 +876,7 @@ bool item::covers( const sub_bodypart_id &bp ) const } } - return does_cover; + return does_cover || subpart_cover; } bool item::covers( const bodypart_id &bp ) const @@ -898,7 +898,7 @@ bool item::covers( const bodypart_id &bp ) const } } - return does_cover; + return does_cover || subpart_cover; } cata::optional item::covers_overlaps( const item &rhs ) const diff --git a/tests/coverage_test.cpp b/tests/coverage_test.cpp index 2fdd4387f1041..b2a69ce05e16f 100644 --- a/tests/coverage_test.cpp +++ b/tests/coverage_test.cpp @@ -221,14 +221,17 @@ TEST_CASE( "Ghost ablative vest", "[coverage]" ) { SECTION( "Ablative not covered" ) { item full = item( "test_ghost_vest" ); - item esapi1 = item( "test_plate" ); - item esapi2 = item( "test_plate" ); - full.put_in( esapi1, item_pocket::pocket_type::CONTAINER ); - full.put_in( esapi2, item_pocket::pocket_type::CONTAINER ); + full.force_insert_item( item( "test_plate" ), item_pocket::pocket_type::CONTAINER ); + full.force_insert_item( item( "test_plate" ), item_pocket::pocket_type::CONTAINER ); item empty = item( "test_ghost_vest" ); + + // make sure vest only covers torso_upper when it has armor in it + REQUIRE( full.covers( sub_bodypart_id( "torso_upper" ) ) ); + REQUIRE( !empty.covers( sub_bodypart_id( "torso_upper" ) ) ); const float dmg_full = get_avg_melee_dmg( full ); const float dmg_empty = get_avg_melee_dmg( empty ); - check_near( "Average damage", dmg_full, dmg_empty, 0.2f ); + // make sure the armor is counting even if the base vest doesn't do anything + check_not_near( "Average damage", dmg_full, dmg_empty, 0.5f ); } }