From 5fb02895907f944bdd0b9c02c3ec20276c8dba65 Mon Sep 17 00:00:00 2001 From: Joshua Chin Date: Sat, 4 Sep 2021 11:05:54 -0400 Subject: [PATCH] Add support for partial armor coverage for monsters (#51304) * Modify load_damage_array to specify default * Add weakpoint.h * Implement weakpoint.cpp * Various compiler fixes * Add weakpoints to mtype. * Let weakpoints modify monster armor. * Compiler fixes * weakpoint bugfixes. * Run astyle * Add json documentation * Run json_formatter * Run clang tidy * Fix GCC-only warning * clang-tidy * Clarify weakpoint docs and switch to percentage coverage. * Rename armor offset to armor penalty, flipping the sign. --- data/json/monsters/misc.json | 2 + doc/MONSTERS.md | 13 ++++++ src/damage.cpp | 5 ++- src/damage.h | 3 +- src/monster.cpp | 11 +++++- src/monstergenerator.cpp | 6 +++ src/mtype.cpp | 1 + src/mtype.h | 2 + src/weakpoint.cpp | 77 ++++++++++++++++++++++++++++++++++++ src/weakpoint.h | 48 ++++++++++++++++++++++ 10 files changed, 164 insertions(+), 4 deletions(-) create mode 100644 src/weakpoint.cpp create mode 100644 src/weakpoint.h diff --git a/data/json/monsters/misc.json b/data/json/monsters/misc.json index fa6229dc48077..8b51827e63795 100644 --- a/data/json/monsters/misc.json +++ b/data/json/monsters/misc.json @@ -17,6 +17,8 @@ "morale": 100, "melee_cut": 0, "vision_day": 1, + "armor_bullet": 100, + "weakpoints": [ { "name": "knee", "armor_mult": { "bullet": 0.5 }, "armor_penalty": { "bullet": 25 }, "coverage": 50 } ], "harvest": "exempt", "death_function": { "corpse_type": "NO_CORPSE", "message": "The %s melts away." }, "regenerates": 50, diff --git a/doc/MONSTERS.md b/doc/MONSTERS.md index 32c3b73870e22..9d13314f0d984 100644 --- a/doc/MONSTERS.md +++ b/doc/MONSTERS.md @@ -63,6 +63,7 @@ Monsters may also have any of these optional properties: | `armor_stab` | (integer) Monster's protection from stab damage | `armor_acid` | (integer) Monster's protection from acid damage | `armor_fire` | (integer) Monster's protection from fire damage +| `weakpoints` | (array of objects) Weakpoints in the monster's protection | `vision_day` | (integer) Vision range in full daylight, with `50` being the typical maximum | `vision_night` | (integer) Vision range in total darkness, ex. coyote `5`, bear `10`, sewer rat `30`, flaming eye `40` | `tracking_distance` | (integer) Amount of tiles the monster will keep between itself and its current tracked enemy or followed leader. Defaults to `3`. @@ -315,6 +316,18 @@ Amount of cutting damage added to die roll on monster melee attack. Monster protection from bashing, cutting, stabbing, acid and fire damage. +## "weakpoints" +(array of objects, optional) + +Weakpoints in the monster's protection. + +| field | description +| --- | --- +| `name` | Name of the weakpoint. +| `coverage` | Base percentage chance of hitting the weakpoint. May be increased by skill level. (e.g. A coverage of 5 means a 5% base chance of hitting the weakpoint) +| `armor_multiplier` | multipler on the monster's base protection when hitting the weakpoint. +| `armor_penalty` | a flat penalty to the monster's protection, applied after the multiplier. + ## "vision_day", "vision_night" (integer, optional) diff --git a/src/damage.cpp b/src/damage.cpp index aec9c8f4085bc..10598c4027519 100644 --- a/src/damage.cpp +++ b/src/damage.cpp @@ -430,10 +430,11 @@ damage_instance load_damage_instance_inherit( const JsonArray &jarr, const damag return di; } -std::array( damage_type::NUM )> load_damage_array( const JsonObject &jo ) +std::array( damage_type::NUM )> load_damage_array( const JsonObject &jo, + float default_value ) { std::array( damage_type::NUM )> ret; - float init_val = jo.get_float( "all", 0.0f ); + float init_val = jo.get_float( "all", default_value ); float phys = jo.get_float( "physical", init_val ); ret[ static_cast( damage_type::BASH ) ] = jo.get_float( "bash", phys ); diff --git a/src/damage.h b/src/damage.h index 53236657eba49..3a5093ed616a6 100644 --- a/src/damage.h +++ b/src/damage.h @@ -151,6 +151,7 @@ resistances load_resistances_instance( const JsonObject &jo ); // Returns damage or resistance data // Handles some shorthands -std::array( damage_type::NUM )> load_damage_array( const JsonObject &jo ); +std::array( damage_type::NUM )> load_damage_array( const JsonObject &jo, + float default_value = 0.0f ); #endif // CATA_SRC_DAMAGE_H diff --git a/src/monster.cpp b/src/monster.cpp index 40e28d0e496fd..eb99591efba5d 100644 --- a/src/monster.cpp +++ b/src/monster.cpp @@ -63,6 +63,7 @@ #include "trap.h" #include "units.h" #include "viewer.h" +#include "weakpoint.h" #include "weather.h" static const efftype_id effect_badpoison( "badpoison" ); @@ -1503,10 +1504,18 @@ bool monster::block_hit( Creature *, bodypart_id &, damage_instance & ) void monster::absorb_hit( const bodypart_id &, damage_instance &dam ) { + resistances r = resistances( *this ); + const weakpoint *weakpoint = type->weakpoints.select_weakpoint(); + weakpoint->apply_to( r ); for( auto &elem : dam.damage_units ) { add_msg_debug( debugmode::DF_MONSTER, "Dam Type: %s :: Ar Pen: %.1f :: Armor Mult: %.1f", name_by_dt( elem.type ), elem.res_pen, elem.res_mult ); - elem.amount -= std::min( resistances( *this ).get_effective_resist( elem ) + + add_msg_debug( debugmode::DF_MONSTER, + "Weakpoint: %s :: Armor Mult: %.1f :: Armor Penalty: %.1f :: Resist: %.1f", + weakpoint->id, weakpoint->armor_mult[static_cast( elem.type )], + weakpoint->armor_penalty[static_cast( elem.type )], + r.get_effective_resist( elem ) ); + elem.amount -= std::min( r.get_effective_resist( elem ) + get_worn_armor_val( elem.type ), elem.amount ); } } diff --git a/src/monstergenerator.cpp b/src/monstergenerator.cpp index 18caa0ff8eac0..f5a0ed3611391 100644 --- a/src/monstergenerator.cpp +++ b/src/monstergenerator.cpp @@ -34,6 +34,7 @@ #include "rng.h" #include "translations.h" #include "units.h" +#include "weakpoint.h" namespace behavior { @@ -718,6 +719,11 @@ void mtype::load( const JsonObject &jo, const std::string &src ) assign( jo, "armor_acid", armor_acid, strict, 0 ); assign( jo, "armor_fire", armor_fire, strict, 0 ); + if( jo.has_array( "weakpoints" ) ) { + weakpoints.clear(); + weakpoints.load( jo.get_array( "weakpoints" ) ); + } + optional( jo, was_loaded, "bleed_rate", bleed_rate, 100 ); assign( jo, "vision_day", vision_day, strict, 0 ); diff --git a/src/mtype.cpp b/src/mtype.cpp index f17da63217314..01b564ceda3b5 100644 --- a/src/mtype.cpp +++ b/src/mtype.cpp @@ -14,6 +14,7 @@ #include "monstergenerator.h" #include "translations.h" #include "units.h" +#include "weakpoint.h" static const itype_id itype_bone( "bone" ); static const itype_id itype_bone_tainted( "bone_tainted" ); diff --git a/src/mtype.h b/src/mtype.h index c877a6dd54cf2..bf0f5db0bdf1e 100644 --- a/src/mtype.h +++ b/src/mtype.h @@ -22,6 +22,7 @@ #include "translations.h" #include "type_id.h" #include "units.h" // IWYU pragma: keep +#include "weakpoint.h" class Creature; class monster; @@ -331,6 +332,7 @@ struct mtype { int armor_bullet = -1; /** innate armor vs. bullet */ int armor_acid = -1; /** innate armor vs. acid */ int armor_fire = -1; /** innate armor vs. fire */ + ::weakpoints weakpoints; // Bleed rate in percent, 0 makes the monster immune to bleeding int bleed_rate = 100; diff --git a/src/weakpoint.cpp b/src/weakpoint.cpp new file mode 100644 index 0000000000000..a04c3d4371bc7 --- /dev/null +++ b/src/weakpoint.cpp @@ -0,0 +1,77 @@ +#include "weakpoint.h" + +#include +#include + +#include "assign.h" +#include "creature.h" +#include "damage.h" +#include "item.h" +#include "rng.h" + +class JsonArray; +class JsonObject; + +weakpoint::weakpoint() +{ + // arrays must be filled manually to avoid UB. + armor_mult.fill( 1.0f ); + armor_penalty.fill( 0.0f ); +} + +void weakpoint::load( const JsonObject &jo ) +{ + assign( jo, "id", id ); + assign( jo, "name", name ); + assign( jo, "coverage", coverage, false, 0.0f, 100.0f ); + if( jo.has_object( "armor_mult" ) ) { + armor_mult = load_damage_array( jo.get_object( "armor_mult" ), 1.0f ); + } + if( jo.has_object( "armor_penalty" ) ) { + armor_penalty = load_damage_array( jo.get_object( "armor_penalty" ), 0.0f ); + } + // Set the ID to the name, if not provided. + if( id.empty() ) { + id = name; + } +} + +void weakpoint::apply_to( resistances &resistances ) const +{ + for( int i = 0; i < static_cast( damage_type::NUM ); ++i ) { + resistances.resist_vals[i] *= armor_mult[i]; + resistances.resist_vals[i] -= armor_penalty[i]; + } +} + +float weakpoint::hit_chance() const +{ + return coverage; +} + +const weakpoint *weakpoints::select_weakpoint() const +{ + float idx = rng_float( 0.0f, 100.0f ); + for( const weakpoint &weakpoint : weakpoint_list ) { + float hit_chance = weakpoint.hit_chance( ); + if( hit_chance <= idx ) { + return &weakpoint; + } + idx -= hit_chance; + } + return &default_weakpoint; +} + +void weakpoints::clear() +{ + weakpoint_list.clear(); +} + +void weakpoints::load( const JsonArray &ja ) +{ + for( const JsonObject jo : ja ) { + weakpoint tmp; + tmp.load( jo ); + weakpoint_list.push_back( std::move( tmp ) ); + } +} diff --git a/src/weakpoint.h b/src/weakpoint.h new file mode 100644 index 0000000000000..f3fc39c313577 --- /dev/null +++ b/src/weakpoint.h @@ -0,0 +1,48 @@ +#pragma once +#ifndef CATA_SRC_WEAKPOINT_H +#define CATA_SRC_WEAKPOINT_H + +#include +#include +#include + +#include "creature.h" +#include "damage.h" + +class JsonArray; +class JsonObject; + +struct weakpoint { + // ID of the weakpoint. Equal to the name, if not provided. + std::string id; + // Name of the weakpoint. Can be empty. + std::string name; + // Percent chance of hitting the weakpoint. Can be increased by skill. + float coverage = 100.0f; + // Multiplier for existing armor values. Defaults to 1. + std::array( damage_type::NUM )> armor_mult; + // Flat penalty to armor values. Applied after the multiplier. + std::array( damage_type::NUM )> armor_penalty; + + weakpoint(); + // Apply the armor multipliers and offsets to a set of resistances. + void apply_to( resistances &resistances ) const; + // Return the change of the creature hitting the weakpoint. + float hit_chance( ) const; + void load( const JsonObject &jo ); +}; + +struct weakpoints { + // List of weakpoints. Each weakpoint should have a unique id. + std::vector weakpoint_list; + // Default weakpoint to return. + weakpoint default_weakpoint; + + // Selects a weakpoint to hit. + const weakpoint *select_weakpoint( ) const; + + void clear(); + void load( const JsonArray &ja ); +}; + +#endif // CATA_SRC_WEAKPOINT_H \ No newline at end of file