From 6dcdd710458eaff4350987f56927c121d364435e Mon Sep 17 00:00:00 2001 From: Chaosvolt Date: Sun, 21 May 2023 17:30:45 -0500 Subject: [PATCH] Shields can interact with ranged attacks, add some vanilla shields (#2851) * Commit what I've got for the moment * Apply suggestions from code review Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Update character.cpp * Update character.cpp * [Eternal Screaming] * Update src/creature.h Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Fix it up to a basic working state * Continue code tweaks, add ballistic shield * And some hopefully final touches * Commit what I've got for the moment * Apply suggestions from code review Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Update character.cpp * Update character.cpp * [Eternal Screaming] * Update src/creature.h Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Fix it up to a basic working state * Continue code tweaks, add ballistic shield * Update JSON_FLAGS.md * Update src/character.cpp Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * refactor: extract covered by shield * refactor: extract shieldlevel * refactor: extract damage calculation * refactor: use switch instead using map is kinda overkill * Fix a slight recipe inconsistency I spotted --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: scarf --- .../itemgroups/Clothing_Gear/clothing.json | 14 +- data/json/itemgroups/Clothing_Gear/gear.json | 4 +- .../Locations_MapExtras/locations.json | 1 + .../locations_commercial.json | 3 +- .../Locations_MapExtras/mansion.json | 7 +- .../monster_drops_lairs.json | 3 +- data/json/itemgroups/art_antiques_crafts.json | 2 + data/json/items/armor/shields.json | 88 +++++++++++ data/json/monsterdrops/zombie_cop.json | 6 +- data/json/monsterdrops/zombie_soldier.json | 3 +- data/json/recipes/armor/other.json | 32 ++++ doc/JSON_FLAGS.md | 18 ++- src/character.cpp | 146 ++++++++++++++++++ src/character.h | 3 + src/creature.cpp | 3 +- src/creature.h | 4 + src/monster.cpp | 5 + src/monster.h | 1 + 18 files changed, 331 insertions(+), 12 deletions(-) create mode 100644 data/json/items/armor/shields.json diff --git a/data/json/itemgroups/Clothing_Gear/clothing.json b/data/json/itemgroups/Clothing_Gear/clothing.json index f41158258a24..8caa18cb9972 100644 --- a/data/json/itemgroups/Clothing_Gear/clothing.json +++ b/data/json/itemgroups/Clothing_Gear/clothing.json @@ -1872,7 +1872,8 @@ [ "beret_wool", 40 ], [ "elbow_pads", 50 ], [ "knee_pads", 50 ], - [ "solarpack", 5 ] + [ "solarpack", 5 ], + [ "shield_ballistic", 3 ] ] }, { @@ -1903,6 +1904,7 @@ [ "elbow_pads", 40 ], [ "knee_pads", 40 ], [ "mask_bal", 5 ], + [ "shield_ballistic", 2 ], [ "e_tool", 10 ], [ "waterproof_gunmod", 8 ], [ "grapnel", 3 ], @@ -2398,7 +2400,11 @@ [ "survivor_shavingkit", 3 ], [ "survivor_hairtrimmer", 1 ], [ "survivor_scope", 1 ], - [ "survnote", 30 ] + [ "survnote", 30 ], + [ "shield_wooden", 1 ], + [ "shield_wooden_large", 1 ], + [ "shield_riot", 3 ], + [ "shield_ballistic", 2 ] ] }, { @@ -2429,7 +2435,9 @@ [ "legguard_metal", 10 ], [ "helmet_corinthian", 45 ], [ "armor_cuirass", 25 ], - [ "legguard_bronze", 20 ] + [ "legguard_bronze", 20 ], + [ "shield_wooden", 10 ], + [ "shield_wooden_large", 5 ] ] }, { diff --git a/data/json/itemgroups/Clothing_Gear/gear.json b/data/json/itemgroups/Clothing_Gear/gear.json index bdf8f537e8e0..42976d5618da 100644 --- a/data/json/itemgroups/Clothing_Gear/gear.json +++ b/data/json/itemgroups/Clothing_Gear/gear.json @@ -31,7 +31,9 @@ [ "suppressor_compact", 20 ], [ "swat_armor", 20 ], [ "tac_fullhelmet", 5 ], - [ "tac_helmet", 10 ] + [ "tac_helmet", 10 ], + [ "shield_riot", 20 ], + [ "shield_ballistic", 5 ] ] }, { diff --git a/data/json/itemgroups/Locations_MapExtras/locations.json b/data/json/itemgroups/Locations_MapExtras/locations.json index 3ac031d246df..f90e2e3a47c9 100644 --- a/data/json/itemgroups/Locations_MapExtras/locations.json +++ b/data/json/itemgroups/Locations_MapExtras/locations.json @@ -1385,6 +1385,7 @@ [ "gloves_tactical", 10 ], [ "armguard_hard", 20 ], [ "legguard_hard", 20 ], + [ "shield_riot", 20 ], [ "emergency_book", 1 ] ] }, diff --git a/data/json/itemgroups/Locations_MapExtras/locations_commercial.json b/data/json/itemgroups/Locations_MapExtras/locations_commercial.json index 345914744961..82b406064213 100644 --- a/data/json/itemgroups/Locations_MapExtras/locations_commercial.json +++ b/data/json/itemgroups/Locations_MapExtras/locations_commercial.json @@ -585,7 +585,8 @@ [ "silver_medal", 2 ], [ "gold_medal", 1 ], [ "bionic_scanner", 5 ], - { "group": "tinware", "prob": 10 } + { "group": "tinware", "prob": 10 }, + [ "shield_riot", 5 ] ] }, { diff --git a/data/json/itemgroups/Locations_MapExtras/mansion.json b/data/json/itemgroups/Locations_MapExtras/mansion.json index c5386f275d4b..e17239cb7651 100644 --- a/data/json/itemgroups/Locations_MapExtras/mansion.json +++ b/data/json/itemgroups/Locations_MapExtras/mansion.json @@ -468,7 +468,7 @@ "id": "soa_mail", "type": "item_group", "subtype": "collection", - "items": [ { "group": "soa_weapons_mail" }, [ "chainmail_suit", 100 ] ] + "items": [ { "group": "soa_weapons_mail" }, { "group": "soa_shields_mail", "prob": 50 }, [ "chainmail_suit", 100 ] ] }, { "id": "soa_weapons_mail", @@ -480,6 +480,11 @@ { "group": "soa_real_weapon_mail", "prob": 3 } ] }, + { + "id": "soa_shields_mail", + "type": "item_group", + "items": [ [ "shield_wooden", 10 ], [ "shield_wooden_large", 5 ] ] + }, { "id": "soa_samurai", "type": "item_group", diff --git a/data/json/itemgroups/Monsters_Animals_Lairs/monster_drops_lairs.json b/data/json/itemgroups/Monsters_Animals_Lairs/monster_drops_lairs.json index a4be1b0e1099..ee4e9cad2b31 100644 --- a/data/json/itemgroups/Monsters_Animals_Lairs/monster_drops_lairs.json +++ b/data/json/itemgroups/Monsters_Animals_Lairs/monster_drops_lairs.json @@ -324,7 +324,8 @@ { "item": "pur_tablets", "prob": 10 }, { "item": "pastaextruder", "prob": 10 }, { "item": "can_sealer", "prob": 10 }, - { "item": "remotevehcontrol", "prob": 8 } + { "item": "remotevehcontrol", "prob": 8 }, + { "item": "shield_ballistic", "prob": 10 } ] }, { diff --git a/data/json/itemgroups/art_antiques_crafts.json b/data/json/itemgroups/art_antiques_crafts.json index 286961408d3e..636789f3dff7 100644 --- a/data/json/itemgroups/art_antiques_crafts.json +++ b/data/json/itemgroups/art_antiques_crafts.json @@ -133,6 +133,8 @@ { "item": "tinderbox", "prob": 4 }, { "item": "flint_steel", "prob": 7 }, { "item": "canteen_wood", "prob": 5 }, + { "item": "shield_wooden", "prob": 3 }, + { "item": "shield_wooden_large", "prob": 3 }, { "item": "apron_leather", "prob": 1 }, { "item": "pot_copper", "prob": 3 }, { "group": "tinware", "prob": 10 } diff --git a/data/json/items/armor/shields.json b/data/json/items/armor/shields.json new file mode 100644 index 000000000000..56abc11fbfbe --- /dev/null +++ b/data/json/items/armor/shields.json @@ -0,0 +1,88 @@ +[ + { + "id": "shield_wooden", + "type": "ARMOR", + "name": { "str": "wooden shield" }, + "description": "A crude wooden shield, lacking any metal or leather reinforcement. Tolerable weight but not very tough.", + "weight": "3 kg", + "volume": "3 L", + "price": "50 USD", + "price_postapoc": "5 USD", + "to_hit": -1, + "bashing": 8, + "material": [ "wood" ], + "symbol": "[", + "color": "brown", + "covers": [ "arm_either", "hand_either" ], + "coverage": 90, + "encumbrance": 15, + "material_thickness": 3, + "techniques": [ "WBLOCK_2" ], + "flags": [ "OVERSIZE", "BELTED", "RESTRICT_HANDS", "BLOCK_WHILE_WORN" ] + }, + { + "id": "shield_wooden_large", + "type": "ARMOR", + "name": { "str": "large wooden shield" }, + "description": "An crude wooden tower shield, lacking any metal or leather reinforcement. Bulky, but offers a decent amount of protection.", + "weight": "5 kg", + "volume": "5 L", + "price": "60 USD", + "price_postapoc": "750 cent", + "to_hit": -2, + "bashing": 10, + "material": [ "wood" ], + "symbol": "[", + "color": "brown", + "covers": [ "arm_either", "hand_either" ], + "coverage": 90, + "encumbrance": 25, + "material_thickness": 3, + "techniques": [ "WBLOCK_3" ], + "flags": [ "OVERSIZE", "BELTED", "RESTRICT_HANDS", "BLOCK_WHILE_WORN" ] + }, + { + "id": "shield_riot", + "type": "ARMOR", + "name": { "str": "riot shield" }, + "description": "A large but fairly light plastic shield, designed for riot police officers. Not too encumbering, but designed for fending off thrown rocks rather than bullets.", + "weight": "2500 g", + "volume": "5 L", + "price": "200 USD", + "price_postapoc": "25 USD", + "to_hit": -1, + "bashing": 4, + "material": [ "plastic" ], + "symbol": "[", + "color": "light_gray", + "covers": [ "arm_either", "hand_either" ], + "coverage": 100, + "encumbrance": 10, + "material_thickness": 4, + "environmental_protection": 3, + "techniques": [ "WBLOCK_3" ], + "flags": [ "OVERSIZE", "BELTED", "RESTRICT_HANDS", "BLOCK_WHILE_WORN" ] + }, + { + "id": "shield_ballistic", + "type": "ARMOR", + "name": { "str": "ballistic shield" }, + "description": "A heavy composite shield used by SWAT teams and other armed forces. It can handle the occasional pistol bullet, but its heavy-duty nature means it's quite encumbering and doesn't cover the legs very well.", + "weight": "8 kg", + "volume": "4 L", + "price": "1000 USD", + "price_postapoc": "50 USD", + "to_hit": -3, + "bashing": 12, + "material": [ "ceramic", "kevlar" ], + "symbol": "[", + "color": "light_gray", + "covers": [ "arm_either", "hand_either" ], + "coverage": 100, + "encumbrance": 30, + "material_thickness": 6, + "environmental_protection": 2, + "techniques": [ "WBLOCK_2" ], + "flags": [ "OVERSIZE", "BELTED", "RESTRICT_HANDS", "BLOCK_WHILE_WORN" ] + } +] diff --git a/data/json/monsterdrops/zombie_cop.json b/data/json/monsterdrops/zombie_cop.json index fb955cb5b1b2..b6ffe66e4b5f 100644 --- a/data/json/monsterdrops/zombie_cop.json +++ b/data/json/monsterdrops/zombie_cop.json @@ -20,7 +20,8 @@ "collection": [ { "item": "badge_deputy", "prob": 100 }, { "item": "badge_detective", "prob": 20 } ], "prob": 10 }, - { "item": "cash_card", "charges-min": 0, "charges-max": 50000 } + { "item": "cash_card", "charges-min": 0, "charges-max": 50000 }, + { "item": "shield_riot", "prob": 5, "damage": [ 1, 4 ] } ] }, { @@ -98,7 +99,8 @@ { "group": "clothing_glasses", "prob": 5 }, { "group": "clothing_watch", "prob": 10 }, { "item": "badge_swat", "prob": 10 }, - { "item": "cash_card", "charges-min": 0, "charges-max": 50000, "prob": 50 } + { "item": "cash_card", "charges-min": 0, "charges-max": 50000, "prob": 50 }, + { "item": "shield_riot", "prob": 10, "damage": [ 1, 4 ] } ] }, { diff --git a/data/json/monsterdrops/zombie_soldier.json b/data/json/monsterdrops/zombie_soldier.json index 29a81c9f01cd..fea68524f8ae 100644 --- a/data/json/monsterdrops/zombie_soldier.json +++ b/data/json/monsterdrops/zombie_soldier.json @@ -50,7 +50,8 @@ "collection": [ { "group": "infantry_officer_gear", "prob": 90 }, { "group": "infantry_medical_gear", "prob": 80 } ] }, { "item": "cash_card", "prob": 10, "charges-min": 0, "charges-max": 50000 }, - { "group": "misc_smoking", "prob": 30 } + { "group": "misc_smoking", "prob": 30 }, + { "item": "shield_ballistic", "prob": 10, "damage": [ 1, 4 ] } ] }, { diff --git a/data/json/recipes/armor/other.json b/data/json/recipes/armor/other.json index ca62ccbbd770..ef11a219ee31 100644 --- a/data/json/recipes/armor/other.json +++ b/data/json/recipes/armor/other.json @@ -400,5 +400,37 @@ "time": "4 m", "autolearn": true, "components": [ [ [ "water", 1 ], [ "water_clean", 1 ] ], [ [ "towel_soiled", 1 ] ] ] + }, + { + "result": "shield_wooden", + "type": "recipe", + "category": "CC_ARMOR", + "subcategory": "CSC_ARMOR_OTHER", + "skill_used": "fabrication", + "difficulty": 2, + "time": "30 m", + "autolearn": true, + "qualities": [ { "id": "CUT", "level": 1 }, { "id": "HAMMER", "level": 1 } ], + "components": [ + [ [ "nail", 8 ] ], + [ [ "2x4", 4 ], [ "wood_panel", 1 ] ], + [ [ "rag", 2 ], [ "felt_patch", 2 ], [ "fur", 2 ], [ "leather", 2 ] ] + ] + }, + { + "result": "shield_wooden_large", + "type": "recipe", + "category": "CC_ARMOR", + "subcategory": "CSC_ARMOR_OTHER", + "skill_used": "fabrication", + "difficulty": 3, + "time": "50 m", + "autolearn": true, + "qualities": [ { "id": "CUT", "level": 1 }, { "id": "HAMMER", "level": 1 } ], + "components": [ + [ [ "nail", 16 ] ], + [ [ "2x4", 8 ], [ "wood_panel", 2 ] ], + [ [ "rag", 2 ], [ "felt_patch", 2 ], [ "fur", 2 ], [ "leather", 2 ] ] + ] } ] diff --git a/doc/JSON_FLAGS.md b/doc/JSON_FLAGS.md index 9d6d199a4910..5957c1aa3a52 100644 --- a/doc/JSON_FLAGS.md +++ b/doc/JSON_FLAGS.md @@ -64,6 +64,7 @@ - [Skills](#skills) - [Tags](#tags) - [Techniques](#techniques) + - [WBLOCK_X](#wblock-x) - [Tools](#tools) - [Flags](#flags-12) - [Flags that apply to items](#flags-that-apply-to-items) @@ -239,7 +240,7 @@ Some armor flags, such as `WATCH` and `ALARMCLOCK` are compatible with other ite - ```BAROMETER``` This gear is equipped with an accurate barometer (which is used to measure atmospheric pressure). - ```BELTED``` Layer for backpacks and things worn over outerwear. - ```BLIND``` Blinds the wearer while worn, and provides nominal protection v. flashbang flashes. -- ```BLOCK_WHILE_WORN``` Allows worn armor or shields to be used for blocking attacks. +- ```BLOCK_WHILE_WORN``` Allows worn armor or shields to be used for blocking attacks. See also the `Techniques` section. - ```BULLET_IMMNUE``` Wearing an item with this flag makes you immune to bullet damage - ```CLIMATE_CONTROL``` This piece of clothing has climate control of some sort, keeping you warmer or cooler depending on ambient and bodily temperature. - ```COLLAR``` This piece of clothing has a wide collar that can keep your mouth warm. @@ -1260,6 +1261,21 @@ Techniques may be used by tools, armors, weapons and anything else that can be w - See contents of `data/json/techniques.json`. - Techniques are also used with martial arts styles, see `data/json/martialarts.json`. +### WBLOCK_X + +The following weapon techniques have some additional usage. These are defensive techniques that allow the item to assist in blocking attacks in melee, with some additional special uses. + +- ```WBLOCK_1``` "Medium blocking ability" +- ```WBLOCK_2``` "High blocking ability" +- ```WBLOCK_3``` "Very high blocking ability" + +An item with one of these techniques can be wielded to provide a bonus to damage reduced by blocking compared, or armor with the `BLOCK_WHILE_WORN` flag can also provide the use of this bonus while wearing the item, serving as a shield. Additionally, wielding or wearing an item with a combination of one of these techniques plus said flag will allow the item to block projectiles aimed at body parts the item otherwise does not cover (or is not covering, in the case of wielded items that meet those prerequisites). The chance that this will happen is based on the `coverage` percentage of the item used for its normal armor value, reduced by a penalty that depends on which blocking technique it possesses. The chance of it intercepting shots that strike the legs (again, unless the armor was set to cover the legs by default already, in which case it uses `coverage` as normal) is furtther penalized. The feet will always be vulnerable unless (for whatever reason a JSON author may devise, forcefield items for example) an item happens to be a shield that already covers the feet as armor. + + Technique | Chance to intercept (head, torso, opposing arm, etc) | Chance to intercept (legs) +-----------|------------------------------------------------------|------------------------------- + WBLOCK_1 | 90% of default coverage value | 75% of default coverage value + WBLOCK_2 | 90% of default coverage value | 75% of default coverage value + WBLOCK_3 | 90% of default coverage value | 75% of default coverage value ## Tools diff --git a/src/character.cpp b/src/character.cpp index 176449ee5f69..ccf4658f3139 100644 --- a/src/character.cpp +++ b/src/character.cpp @@ -30,6 +30,7 @@ #include "consumption.h" #include "coordinate_conversions.h" #include "coordinates.h" +#include "damage.h" #include "debug.h" #include "disease.h" #include "effect.h" @@ -57,6 +58,7 @@ #include "mapdata.h" #include "material.h" #include "math_defines.h" +#include "martialarts.h" #include "memorial_logger.h" #include "messages.h" #include "mission.h" @@ -114,6 +116,10 @@ static const activity_id ACT_WAIT_STAMINA( "ACT_WAIT_STAMINA" ); static const bionic_id bio_eye_optic( "bio_eye_optic" ); static const bionic_id bio_watch( "bio_watch" ); +static const matec_id WBLOCK_1( "WBLOCK_1" ); +static const matec_id WBLOCK_2( "WBLOCK_2" ); +static const matec_id WBLOCK_3( "WBLOCK_3" ); + static const efftype_id effect_adrenaline( "adrenaline" ); static const efftype_id effect_ai_waiting( "ai_waiting" ); static const efftype_id effect_alarm_clock( "alarm_clock" ); @@ -10631,6 +10637,146 @@ bool Character::uncanny_dodge() return character_funcs::try_uncanny_dodge( *this ); } +namespace +{ + +auto is_foot_hit( const bodypart_id &bp_hit ) -> bool +{ + return bp_hit == bodypart_str_id( "foot_l" ) || bp_hit == bodypart_str_id( "foot_r" ); +} + +auto is_leg_hit( const bodypart_id &bp_hit ) -> bool +{ + return bp_hit == bodypart_str_id( "leg_l" ) || bp_hit == bodypart_str_id( "leg_r" ); +} + +/** + * @brief Check if the given shield can protect the given bodypart. + * + * - Best item available doesn't count as a shield. + * - Shield already protects the part we're interested in. + * - Targeted bodypart is a foot, unlikely to ever successfully block that low. + */ +auto is_covered_by_shield( const bodypart_id &bp_hit, const item &shield ) -> bool +{ + return shield.has_flag( "BLOCK_WHILE_WORN" ) + && !shield.covers( bp_hit->token ) + && !is_foot_hit( bp_hit ); +} + +enum class ShieldLevel { None, Block1, Block2, Block3 }; +auto shield_level( const item &shield ) -> ShieldLevel +{ + if( shield.has_technique( WBLOCK_3 ) ) { + return ShieldLevel::Block3; + } else if( shield.has_technique( WBLOCK_2 ) ) { + return ShieldLevel::Block2; + } else if( shield.has_technique( WBLOCK_1 ) ) { + return ShieldLevel::Block1; + } + return ShieldLevel::None; +} + +auto coverage_modifier_by_technic( ShieldLevel level, bool leg_hit ) -> float +{ + switch( level ) { + case ShieldLevel::Block3: + return leg_hit ? 0.75f : 0.9f; + case ShieldLevel::Block2: + return leg_hit ? 0.5f : 0.8f; + case ShieldLevel::Block1: + return leg_hit ? 0.25f : 0.7f; + default: + return 0.0f; + } +} + +auto is_valid_hallucination( Creature *source ) -> bool +{ + return source != nullptr && source->is_hallucination(); +} + +auto get_shield_resist( const item &shield, const damage_unit &damage ) -> int +{ + // *INDENT-OFF* + switch( damage.type ) { + case DT_BASH: return shield.bash_resist(); + case DT_CUT: return shield.cut_resist(); + case DT_STAB: return shield.stab_resist(); + case DT_BULLET: return shield.bullet_resist(); + case DT_HEAT: return shield.fire_resist(); + case DT_ACID: return shield.acid_resist(); + default: return 0; + } + // *INDENT-ON* +} + +auto get_block_amount( const item &shield, const damage_unit &unit ) -> int +{ + const int resist = get_shield_resist( shield, unit ); + + return std::max( 0.0f, ( resist - unit.res_pen ) * unit.res_mult ); +} + +} // namespace + +bool Character::block_ranged_hit( Creature *source, bodypart_id &bp_hit, damage_instance &dam ) +{ + // Having access to more than one shield is not normal in vanilla, for now keep it simple and only give one chance to catch a bullet. + item &shield = best_shield(); + + // Bail out early just in case, if blocking with bare hands. + if( shield.is_null() ) { + return false; + } + + const auto level = shield_level( shield ); + if( level == ShieldLevel::None || !is_covered_by_shield( bp_hit, shield ) ) { + return false; + } + // Modify chance based on coverage and blocking ability, with lowered chance if hitting the legs. Exclude armguards here. + const float technic_modifier = coverage_modifier_by_technic( level, is_leg_hit( bp_hit ) ); + const float shield_coverage_modifier = shield.get_coverage() * technic_modifier; + + add_msg( m_debug, _( "block_ranged_hit success rate: %i%%" ), + static_cast( shield_coverage_modifier ) ); + + // Now roll coverage to determine if we intercept the shot. + if( rng( 1, 100 ) > shield_coverage_modifier ) { + add_msg( m_debug, _( "block_ranged_hit attempt failed" ) ); + return false; + } + + const float wear_modifier = is_valid_hallucination( source ) ? 0.0f : 1.0f; + handle_melee_wear( shield, wear_modifier ); + + int total_damage = 0; + int blocked_damage = 0; + for( auto &elem : dam.damage_units ) { + total_damage += elem.amount * elem.damage_multiplier; + // Go through all relevant damage types and reduce by armor value if one exists. + const float block_amount = get_block_amount( shield, elem ); + elem.amount -= block_amount; + blocked_damage += block_amount; + } + blocked_damage = std::min( total_damage, blocked_damage ); + add_msg( m_debug, _( "expected base damage: %i" ), total_damage ); + + const std::string thing_blocked_with = shield.tname(); + if( blocked_damage > 0 ) { + add_msg_player_or_npc( + _( "The shot hits your %s, absorbing %i damage." ), + _( "The shot hits 's %s, absorbing %i damage." ), + thing_blocked_with, blocked_damage ); + } else { + add_msg_player_or_npc( + _( "The shot hits your %s, but it punches right through!" ), + _( "The shot hits 's %s, but it punches right through!" ), + thing_blocked_with ); + } + return true; +} + float Character::fall_damage_mod() const { if( has_effect_with_flag( "EFFECT_FEATHER_FALL" ) ) { diff --git a/src/character.h b/src/character.h index 9e4814d8d374..ab7b3c81212f 100644 --- a/src/character.h +++ b/src/character.h @@ -584,6 +584,9 @@ class Character : public Creature, public visitable bool uncanny_dodge() override; + /** Checks for chance that a ranged attack will hit other armor along the way */ + bool block_ranged_hit( Creature *source, bodypart_id &bp_hit, damage_instance &dam ) override; + // melee.cpp /** Checks for valid block abilities and reduces damage accordingly. Returns true if the player blocks */ bool block_hit( Creature *source, bodypart_id &bp_hit, damage_instance &dam ) override; diff --git a/src/creature.cpp b/src/creature.cpp index a05c1542ff4e..cd9bd9547983 100644 --- a/src/creature.cpp +++ b/src/creature.cpp @@ -782,7 +782,8 @@ void Creature::deal_projectile_attack( Creature *source, dealt_projectile_attack impact.mult_damage( 1.0f / dmg_ratio ); } } - + // If we have a shield, it might passively block ranged impacts + block_ranged_hit( source, bp_hit, impact ); dealt_dam = deal_damage( source, bp_hit, impact ); dealt_dam.bp_hit = bp_hit->token; diff --git a/src/creature.h b/src/creature.h index cb9c09db863e..4d9ecac480b5 100644 --- a/src/creature.h +++ b/src/creature.h @@ -239,6 +239,10 @@ class Creature virtual bool block_hit( Creature *source, bodypart_id &bp_hit, damage_instance &dam ) = 0; + // handles interaction of shields and ranged attacks. mutates &dam + virtual bool block_ranged_hit( Creature *source, bodypart_id &bp_hit, + damage_instance &dam ) = 0; + // handles armor absorption (including clothing damage etc) // of damage instance. mutates &dam virtual void absorb_hit( const bodypart_id &bp, damage_instance &dam ) = 0; diff --git a/src/monster.cpp b/src/monster.cpp index 0a779fe1c782..e00f4b4b750c 100644 --- a/src/monster.cpp +++ b/src/monster.cpp @@ -1378,6 +1378,11 @@ bool monster::block_hit( Creature *, bodypart_id &, damage_instance & ) return false; } +bool monster::block_ranged_hit( Creature *, bodypart_id &, damage_instance & ) +{ + return false; +} + void monster::absorb_hit( const bodypart_id &, damage_instance &dam ) { for( auto &elem : dam.damage_units ) { diff --git a/src/monster.h b/src/monster.h index a2fe2a9ea77b..511aa12bf33c 100644 --- a/src/monster.h +++ b/src/monster.h @@ -314,6 +314,7 @@ class monster : public Creature, public visitable void absorb_hit( const bodypart_id &bp, damage_instance &dam ) override; bool block_hit( Creature *source, bodypart_id &bp_hit, damage_instance &d ) override; + bool block_ranged_hit( Creature *source, bodypart_id &bp_hit, damage_instance &d ) override; void melee_attack( Creature &target ); void melee_attack( Creature &target, float accuracy ); void melee_attack( Creature &p, bool ) = delete;