diff --git a/data/json/materials.json b/data/json/materials.json index b9e89b53e2cad..ba1864698e1ce 100644 --- a/data/json/materials.json +++ b/data/json/materials.json @@ -105,6 +105,7 @@ "fire_resist": 1, "elec_resist": 2, "chip_resist": 8, + "wind_resist": 90, "repaired_with": "bone", "salvaged_into": "skewer_bone", "dmg_adj": [ "scratched", "cut", "cracked", "shattered" ], @@ -241,6 +242,7 @@ "fire_resist": 2, "elec_resist": 2, "chip_resist": 10, + "wind_resist": 90, "repaired_with": "chitin_piece", "salvaged_into": "chitin_piece", "dmg_adj": [ "scratched", "cut", "cracked", "shattered" ], @@ -812,6 +814,7 @@ "fire_resist": 2, "elec_resist": 2, "chip_resist": 10, + "wind_resist": 90, "repaired_with": "leather", "salvaged_into": "leather", "dmg_adj": [ "scratched", "cut", "shredded", "tattered" ], @@ -919,6 +922,7 @@ "fire_resist": 20, "elec_resist": 4, "chip_resist": 8, + "wind_resist": 90, "repaired_with": "nomex", "salvaged_into": "nomex", "dmg_adj": [ "ripped", "torn", "shredded", "tattered" ], @@ -1025,6 +1029,7 @@ "fire_resist": 1, "elec_resist": 2, "chip_resist": 6, + "wind_resist": 90, "repaired_with": "plastic_chunk", "salvaged_into": "plastic_chunk", "dmg_adj": [ "scratched", "cut", "cracked", "shattered" ], @@ -1544,6 +1549,7 @@ "elec_resist": 2, "chip_resist": 6, "warmth_when_wet": 0.5, + "wind_resist": 60, "repaired_with": "felt_patch", "salvaged_into": "felt_patch", "dmg_adj": [ "ripped", "torn", "shredded", "tattered" ], diff --git a/doc/JSON_INFO.md b/doc/JSON_INFO.md index e47070ec36283..81318593c64cc 100644 --- a/doc/JSON_INFO.md +++ b/doc/JSON_INFO.md @@ -649,6 +649,7 @@ When you sort your inventory by category, these are the categories that are disp | `dmg_adj` | Adjectives used to describe damage states of a material. | `density` | Density of a material. | `vitamins` | Vitamins in a material. Usually overridden by item specific values. +| `wind_resist` | Percentage 0-100. How effective this material is at stopping wind from getting through. Higher values are better. If none of the materials an item is made of specify a value, a default of 99 is assumed. | `warmth_when_wet` | Percentage of warmth retained when fully drenched. Default is 0.2. | `specific_heat_liquid` | Specific heat of a material when not frozen (J/(g K)). Default 4.186. | `specific_heat_solid` | Specific heat of a material when frozen (J/(g K)). Default 2.108. diff --git a/src/bodypart.cpp b/src/bodypart.cpp index df1919346e269..31e9ff2e521bb 100644 --- a/src/bodypart.cpp +++ b/src/bodypart.cpp @@ -14,6 +14,19 @@ #include "pldata.h" #include "type_id.h" +const bodypart_str_id body_part_head( "head" ); +const bodypart_str_id body_part_eyes( "eyes" ); +const bodypart_str_id body_part_mouth( "mouth" ); +const bodypart_str_id body_part_torso( "torso" ); +const bodypart_str_id body_part_arm_l( "arm_l" ); +const bodypart_str_id body_part_arm_r( "arm_r" ); +const bodypart_str_id body_part_hand_l( "hand_l" ); +const bodypart_str_id body_part_hand_r( "hand_r" ); +const bodypart_str_id body_part_leg_l( "leg_l" ); +const bodypart_str_id body_part_foot_l( "foot_l" ); +const bodypart_str_id body_part_leg_r( "leg_r" ); +const bodypart_str_id body_part_foot_r( "foot_r" ); + side opposite_side( side s ) { switch( s ) { diff --git a/src/bodypart.h b/src/bodypart.h index 8ba9e86d1941f..5acf9abe70c4d 100644 --- a/src/bodypart.h +++ b/src/bodypart.h @@ -15,9 +15,26 @@ class JsonObject; class JsonIn; class JsonOut; +struct body_part_type; template struct enum_traits; +using bodypart_str_id = string_id; +using bodypart_id = int_id; + +extern const bodypart_str_id body_part_head; +extern const bodypart_str_id body_part_eyes; +extern const bodypart_str_id body_part_mouth; +extern const bodypart_str_id body_part_torso; +extern const bodypart_str_id body_part_arm_l; +extern const bodypart_str_id body_part_arm_r; +extern const bodypart_str_id body_part_hand_l; +extern const bodypart_str_id body_part_hand_r; +extern const bodypart_str_id body_part_leg_l; +extern const bodypart_str_id body_part_foot_l; +extern const bodypart_str_id body_part_leg_r; +extern const bodypart_str_id body_part_foot_r; + // The order is important ; pldata.h has to be in the same order enum body_part : int { bp_torso = 0, @@ -69,11 +86,6 @@ constexpr std::array all_body_parts = {{ } }; -struct body_part_type; - -using bodypart_str_id = string_id; -using bodypart_id = int_id; - struct body_part_type { public: bodypart_str_id id; diff --git a/src/character.cpp b/src/character.cpp index 897f3b7ee4f98..44c65ee0c994f 100644 --- a/src/character.cpp +++ b/src/character.cpp @@ -304,6 +304,7 @@ static const std::string flag_ALLOWS_NATURAL_ATTACKS( "ALLOWS_NATURAL_ATTACKS" ) static const std::string flag_AURA( "AURA" ); static const std::string flag_BELTED( "BELTED" ); static const std::string flag_BLIND( "BLIND" ); +static const flag_str_id flag_COLLAR( "COLLAR" ); static const std::string flag_DEAF( "DEAF" ); static const std::string flag_DISABLE_SIGHTS( "DISABLE_SIGHTS" ); static const std::string flag_EFFECT_INVISIBLE( "EFFECT_INVISIBLE" ); @@ -312,6 +313,7 @@ static const std::string flag_FIX_NEARSIGHT( "FIX_NEARSIGHT" ); static const std::string flag_FUNGUS( "FUNGUS" ); static const std::string flag_GNV_EFFECT( "GNV_EFFECT" ); static const std::string flag_HELMET_COMPAT( "HELMET_COMPAT" ); +static const flag_str_id flag_HOOD( "HOOD" ); static const std::string flag_IR_EFFECT( "IR_EFFECT" ); static const std::string flag_ONLY_ONE( "ONLY_ONE" ); static const std::string flag_OUTER( "OUTER" ); @@ -319,6 +321,7 @@ static const std::string flag_OVERSIZE( "OVERSIZE" ); static const std::string flag_PARTIAL_DEAF( "PARTIAL_DEAF" ); static const std::string flag_PERPETUAL( "PERPETUAL" ); static const std::string flag_PERSONAL( "PERSONAL" ); +static const flag_str_id flag_POCKETS( "POCKETS" ); static const std::string flag_PLOWABLE( "PLOWABLE" ); static const std::string flag_POWERARMOR_COMPATIBLE( "POWERARMOR_COMPATIBLE" ); static const std::string flag_RESTRICT_HANDS( "RESTRICT_HANDS" ); @@ -331,6 +334,7 @@ static const std::string flag_SWIMMABLE( "SWIMMABLE" ); static const std::string flag_SWIM_GOGGLES( "SWIM_GOGGLES" ); static const std::string flag_UNDERSIZE( "UNDERSIZE" ); static const std::string flag_USE_UPS( "USE_UPS" ); +static const flag_str_id flag_CLIMATE_CONTROL( "CLIMATE_CONTROL" ); static const mtype_id mon_player_blob( "mon_player_blob" ); static const mtype_id mon_shadow_snake( "mon_shadow_snake" ); @@ -3649,7 +3653,7 @@ bool Character::in_climate_control() if( w.active && w.is_power_armor() ) { return true; } - if( worn_with_flag( "CLIMATE_CONTROL" ) ) { + if( w.has_flag( flag_CLIMATE_CONTROL.str() ) ) { return true; } } @@ -3675,41 +3679,42 @@ bool Character::in_climate_control() return regulated_area; } -int Character::get_wind_resistance( const bodypart_id &bp ) const +std::map Character::get_wind_resistance( const std::map > &clothing_map ) const { - int coverage = 0; - float totalExposed = 1.0; - int totalCoverage = 0; - int penalty = 100; - for( auto &i : worn ) { - if( i.covers( bp->token ) ) { - if( i.made_of( material_id( "leather" ) ) || i.made_of( material_id( "plastic" ) ) || - i.made_of( material_id( "bone" ) ) || - i.made_of( material_id( "chitin" ) ) || i.made_of( material_id( "nomex" ) ) ) { - penalty = 10; // 90% effective - } else if( i.made_of( material_id( "cotton" ) ) ) { - penalty = 30; - } else if( i.made_of( material_id( "wool" ) ) ) { - penalty = 40; - } else { - penalty = 1; // 99% effective - } - - coverage = std::max( 0, i.get_coverage() - penalty ); - totalExposed *= ( 1.0 - coverage / 100.0 ); // Coverage is between 0 and 1? - } + std::map ret; + for( const bodypart_id &bp : get_all_body_parts() ) { + ret.emplace( bp, 0 ); } // Your shell provides complete wind protection if you're inside it if( has_active_mutation( trait_SHELL2 ) ) { - totalCoverage = 100; - return totalCoverage; + for( std::pair &this_bp : ret ) { + this_bp.second = 100; + } + return ret; } - totalCoverage = 100 - totalExposed * 100; + for( const std::pair> &on_bp : clothing_map ) { + const bodypart_id &bp = on_bp.first; + + int coverage = 0; + float totalExposed = 1.0f; + int totalCoverage = 0; + int penalty = 100; + + for( const item *it : on_bp.second ) { + const item &i = *it; + penalty = 100 - i.wind_resist(); + coverage = std::max( 0, i.get_coverage() - penalty ); + totalExposed *= ( 1.0 - coverage / 100.0 ); // Coverage is between 0 and 1? + } - return totalCoverage; + ret[bp] = totalCoverage = 100 - totalExposed * 100; + } + + return ret; } void layer_details::reset() @@ -5078,14 +5083,34 @@ void Character::update_bodytemp( const map &m, weather_manager &weather ) temp_equalizer( bodypart_id( "torso" ), bodypart_id( "leg_r" ) ); temp_equalizer( bodypart_id( "torso" ), bodypart_id( "head" ) ); - temp_equalizer( bodypart_id( "head" ), bodypart_id( "mouth" ) ); - temp_equalizer( bodypart_id( "arm_l" ), bodypart_id( "hand_l" ) ); temp_equalizer( bodypart_id( "arm_r" ), bodypart_id( "hand_r" ) ); temp_equalizer( bodypart_id( "leg_l" ), bodypart_id( "foot_l" ) ); temp_equalizer( bodypart_id( "leg_r" ), bodypart_id( "foot_r" ) ); + std::map> clothing_map; + for( const bodypart_id &bp : get_all_body_parts() ) { + clothing_map.emplace( bp, std::vector() ); + } + for( const item &it : worn ) { + // TODO: Port body part set id changes + const body_part_set &covered = it.get_covered_body_parts(); + for( size_t i = 0; i < num_bp; i++ ) { + body_part token = static_cast( i ); + if( covered.test( token ) ) { + clothing_map[convert_bp( token )].emplace_back( &it ); + } + } + } + + std::map warmth_per_bp = warmth( clothing_map ); + std::map bonus_warmth_per_bp = bonus_item_warmth( clothing_map ); + std::map wind_res_per_bp = get_wind_resistance( clothing_map ); + // We might not use this at all, so leave it empty + // If we do need to use it, we'll initialize it (once) there + std::map fire_armor_per_bp; + // Current temperature and converging temperature calculations for( const bodypart_id &bp : get_all_body_parts() ) { // Skip eyes @@ -5108,12 +5133,12 @@ void Character::update_bodytemp( const map &m, weather_manager &weather ) double scaled_temperature = logarithmic_range( BODYTEMP_VERY_COLD, BODYTEMP_VERY_HOT, temp_cur[bp->token] ); // Produces a smooth curve between 30.0 and 60.0. - double homeostasis_adjustement = 30.0 * ( 1.0 + scaled_temperature ); - int clothing_warmth_adjustement = static_cast( homeostasis_adjustement * warmth( bp ) ); - int clothing_warmth_adjusted_bonus = static_cast( homeostasis_adjustement * bonus_item_warmth( - bp ) ); + double homeostasis_adjustment = 30.0 * ( 1.0 + scaled_temperature ); + int clothing_warmth_adjustment = static_cast( homeostasis_adjustment * warmth_per_bp[bp] ); + int clothing_warmth_adjusted_bonus = static_cast( homeostasis_adjustment * + bonus_warmth_per_bp[bp] ); // WINDCHILL - double bp_windpower = total_windpower * ( 1 - get_wind_resistance( bp ) / 100.0 ); + double bp_windpower = total_windpower * ( 1 - wind_res_per_bp[bp] / 100.0 ); // Calculate windchill int windchill = submerged_bp ? 0 @@ -5125,10 +5150,42 @@ void Character::update_bodytemp( const map &m, weather_manager &weather ) // clothing warmth, and body wetness. int bp_conv = adjusted_temp + windchill * 100 - + clothing_warmth_adjustement + + clothing_warmth_adjustment + mutation_heat_low + sunlight_warmth; + // Bark : lowers blister count to -5; harder to get blisters + // If the counter is high, your skin starts to burn + int blister_count = ( has_bark ? -5 : 0 ); + + if( frostbite_timer[bp->token] > 0 ) { + frostbite_timer[bp->token] -= std::min( 5, h_radiation ); + } + blister_count += h_radiation - 111 > 0 ? + std::max( static_cast( std::sqrt( h_radiation - 111 ) ), 0 ) : 0; + + if( has_heatsink ) { + blister_count -= 20; + } + if( fire_armor_per_bp.empty() && blister_count > 0 ) { + fire_armor_per_bp = get_armor_fire( clothing_map ); + } + // BLISTERS : Skin gets blisters from intense heat exposure. + // Fire protection protects from blisters. + // Heatsinks give near-immunity. + if( blister_count - fire_armor_per_bp[bp] > 0 ) { + add_effect( effect_blisters, 1_turns, bp->token ); + if( pyromania ) { + add_morale( MORALE_PYROMANIA_NEARFIRE, 10, 10, 1_hours, + 30_minutes ); // Proximity that's close enough to harm us gives us a bit of a thrill + rem_morale( MORALE_PYROMANIA_NOFIRE ); + } + } else if( pyromania && best_fire >= 1 ) { // Only give us fire bonus if there's actually fire + add_morale( MORALE_PYROMANIA_NEARFIRE, 5, 5, 30_minutes, + 15_minutes ); // Gain a much smaller mood boost even if it doesn't hurt us + rem_morale( MORALE_PYROMANIA_NOFIRE ); + } + // Climate Control eases the effects of high and low ambient temps if( has_climate_control ) { bp_conv = temp_corrected_by_climate_control( bp_conv ); @@ -5227,31 +5284,6 @@ void Character::update_bodytemp( const map &m, weather_manager &weather ) } } - // Bark : lowers blister count to -5; harder to get blisters - int blister_count = ( has_bark ? -5 : 0 ); // If the counter is high, your skin starts to burn - - if( frostbite_timer[bp->token] > 0 ) { - frostbite_timer[bp->token] -= std::min( 5, h_radiation ); - } - blister_count += h_radiation - 111 > 0 ? - std::max( static_cast( std::sqrt( h_radiation - 111 ) ), 0 ) : 0; - - // BLISTERS : Skin gets blisters from intense heat exposure. - // Fire protection protects from blisters. - // Heatsinks give near-immunity. - if( blister_count - get_armor_fire( bp ) - ( has_heatsink ? 20 : 0 ) > 0 ) { - add_effect( effect_blisters, 1_turns, bp->token ); - if( pyromania ) { - add_morale( MORALE_PYROMANIA_NEARFIRE, 10, 10, 1_hours, - 30_minutes ); // Proximity that's close enough to harm us gives us a bit of a thrill - rem_morale( MORALE_PYROMANIA_NOFIRE ); - } - } else if( pyromania && best_fire >= 1 ) { // Only give us fire bonus if there's actually fire - add_morale( MORALE_PYROMANIA_NEARFIRE, 5, 5, 30_minutes, - 15_minutes ); // Gain a much smaller mood boost even if it doesn't hurt us - rem_morale( MORALE_PYROMANIA_NOFIRE ); - } - // FROSTBITE - only occurs to hands, feet, face /** @@ -5286,11 +5318,11 @@ void Character::update_bodytemp( const map &m, weather_manager &weather ) int wetness_percentage = 100 * body_wetness[bp->token] / drench_capacity[bp->token]; // 0 - 100 // Warmth gives a slight buff to temperature resistance // Wetness gives a heavy nerf to temperature resistance - double adjusted_warmth = warmth( bp ) - wetness_percentage; + double adjusted_warmth = warmth_per_bp.at( bp ) - wetness_percentage; int Ftemperature = static_cast( player_local_temp + 0.2 * adjusted_warmth ); // Windchill reduced by your armor int FBwindPower = static_cast( - total_windpower * ( 1 - get_wind_resistance( bp ) / 100.0 ) ); + total_windpower * ( 1 - wind_res_per_bp[ bp ] / 100.0 ) ); int intense = get_effect_int( effect_frostbite, bp->token ); @@ -6718,6 +6750,7 @@ int Character::get_armor_cut( bodypart_id bp ) const return get_armor_cut_base( bp ) + armor_cut_bonus; } +// TODO: Reduce duplication with below function int Character::get_armor_type( damage_type dt, bodypart_id bp ) const { switch( dt ) { @@ -6754,6 +6787,53 @@ int Character::get_armor_type( damage_type dt, bodypart_id bp ) const return 0; } +std::map Character::get_all_armor_type( damage_type dt, + const std::map> &clothing_map ) const +{ + std::map ret; + for( const bodypart_id &bp : get_all_body_parts() ) { + ret.emplace( bp, 0 ); + } + + for( std::pair &per_bp : ret ) { + const bodypart_id &bp = per_bp.first; + switch( dt ) { + case DT_TRUE: + case DT_BIOLOGICAL: + // Characters cannot resist this + return ret; + /* BASH, CUT, STAB, and BULLET don't benefit from the clothing_map optimization */ + // TODO: Fix that + case DT_BASH: + per_bp.second += get_armor_bash( bp ); + break; + case DT_CUT: + per_bp.second += get_armor_cut( bp ); + break; + case DT_STAB: + per_bp.second += get_armor_cut( bp ) * 0.8f; + break; + case DT_ACID: + case DT_HEAT: + case DT_COLD: + case DT_ELECTRIC: { + for( const item *it : clothing_map.at( bp ) ) { + per_bp.second += it->damage_resist( dt ); + } + + per_bp.second += mutation_armor( bp, dt ); + break; + } + case DT_NULL: + case NUM_DT: + debugmsg( "Invalid damage type: %d", dt ); + return ret; + } + } + + return ret; +} + int Character::get_armor_bash_base( bodypart_id bp ) const { int ret = 0; @@ -7667,7 +7747,8 @@ void Character::rebuild_mutation_cache() } } -double Character::bonus_from_enchantments( double base, enchant_vals::mod value, bool round ) const +double Character::bonus_from_enchantments( double base, enchant_vals::mod value, + bool round ) const { return enchantment_cache->calc_bonus( value, base, round ); } @@ -7963,9 +8044,10 @@ float Character::bionic_armor_bonus( const bodypart_id &bp, damage_type dt ) con return result; } -int Character::get_armor_fire( const bodypart_id &bp ) const +std::map Character::get_armor_fire( const + std::map> &clothing_map ) const { - return get_armor_type( DT_HEAT, bp ); + return get_all_armor_type( DT_HEAT, clothing_map ); } void Character::did_hit( Creature &target ) @@ -7983,7 +8065,8 @@ void Character::on_hit( Creature *source, bodypart_id /*bp_hit*/, Where damage to character is actually applied to hit body parts Might be where to put bleed stuff rather than in player::deal_damage() */ -void Character::apply_damage( Creature *source, bodypart_id hurt, int dam, const bool bypass_med ) +void Character::apply_damage( Creature *source, bodypart_id hurt, int dam, + const bool bypass_med ) { if( is_dead_state() || has_trait( trait_DEBUG_NODMG ) ) { // don't do any more damage if we're already dead @@ -8945,57 +9028,67 @@ std::string Character::is_snuggling() const return "nothing"; } -int Character::warmth( const bodypart_id &bp ) const +std::map Character::warmth( const std::map> + &clothing_map ) const { - int ret = 0; - int warmth = 0; + std::map ret; + for( const bodypart_id &bp : get_all_body_parts() ) { + ret.emplace( bp, 0 ); + } - for( const item &i : worn ) { - if( i.covers( bp->token ) ) { - warmth = i.get_warmth(); + for( const std::pair> &on_bp : clothing_map ) { + const bodypart_id &bp = on_bp.first; + for( const item *it : on_bp.second ) { + double warmth = it->get_warmth(); // Warmth reduced linearly with wetness - const auto &materials = i.made_of(); + const auto &materials = it->made_of(); float max_wet_resistance = std::accumulate( materials.begin(), materials.end(), 0.0f, []( float best, const material_id & mat ) { return std::max( best, mat->warmth_when_wet() ); } ); float wet_mult = 1.0f - max_wet_resistance * body_wetness[bp->token] / drench_capacity[bp->token]; - ret += warmth * wet_mult; + ret[bp] += warmth * wet_mult; } + ret[bp] += get_effect_int( effect_heating_bionic, bp->token ); } - ret += get_effect_int( effect_heating_bionic, bp->token ); return ret; } -static int bestwarmth( const std::list< item > &its, const std::string &flag ) +static int bestwarmth( const std::vector &its, const flag_id &flag ) { int best = 0; - for( auto &w : its ) { - if( w.has_flag( flag ) && w.get_warmth() > best ) { - best = w.get_warmth(); + for( const item *w : its ) { + if( w->has_flag( flag.id().c_str() ) && w->get_warmth() > best ) { + best = w->get_warmth(); } } return best; } -int Character::bonus_item_warmth( const bodypart_id &bp ) const +std::map Character::bonus_item_warmth( const + std::map> &clothing_map ) const { - int ret = 0; - - // If the player is not wielding anything big, check if hands can be put in pockets - if( ( bp == bodypart_id( "hand_l" ) || bp == bodypart_id( "hand_r" ) ) && - weapon.volume() < 500_ml ) { - ret += bestwarmth( worn, "POCKETS" ); + std::map ret; + for( const bodypart_id &bp : get_all_body_parts() ) { + ret.emplace( bp, 0 ); } + for( const std::pair> &on_bp : clothing_map ) { + const bodypart_id &bp = on_bp.first; + // If the player is not wielding anything big, check if hands can be put in pockets + if( ( bp == body_part_hand_l || bp == body_part_hand_r ) && + weapon.volume() < 500_ml ) { + ret[bp] += bestwarmth( on_bp.second, flag_POCKETS ); + } - // If the player's head is not encumbered, check if hood can be put up - if( bp == bodypart_id( "head" ) && encumb( bp_head ) < 10 ) { - ret += bestwarmth( worn, "HOOD" ); - } + // If the player's head is not encumbered, check if hood can be put up + if( bp == body_part_head && encumb( body_part_head->token ) < 10 ) { + ret[bp] += bestwarmth( on_bp.second, flag_HOOD ); + } - // If the player's mouth is not encumbered, check if collar can be put up - if( bp == bodypart_id( "mouth" ) && encumb( bp_mouth ) < 10 ) { - ret += bestwarmth( worn, "COLLAR" ); + // If the player's mouth is not encumbered, check if collar can be put up + if( bp == body_part_mouth && encumb( body_part_mouth->token ) < 10 ) { + ret[bp] += bestwarmth( on_bp.second, flag_COLLAR ); + } } return ret; diff --git a/src/character.h b/src/character.h index 7eb1755862eb9..97790c92fd8e1 100644 --- a/src/character.h +++ b/src/character.h @@ -544,7 +544,8 @@ class Character : public Creature, public visitable bool in_climate_control(); /** Returns wind resistance provided by armor, etc **/ - int get_wind_resistance( const bodypart_id &bp ) const; + std::map get_wind_resistance( const + std::map> &clothing_map ) const; /** Returns true if the player isn't able to see */ bool is_blind() const; @@ -729,8 +730,9 @@ class Character : public Creature, public visitable float bionic_armor_bonus( const bodypart_id &bp, damage_type dt ) const; /** Returns the armor bonus against given type from martial arts buffs */ int mabuff_armor_bonus( damage_type type ) const; - /** Returns overall fire resistance for the body part */ - int get_armor_fire( const bodypart_id &bp ) const; + /** Returns overall fire resistance */ + std::map get_armor_fire( const std::map> + &clothing_map ) const; // --------------- Mutation Stuff --------------- // In newcharacter.cpp /** Returns the id of a random starting trait that costs >= 0 points */ @@ -1732,6 +1734,8 @@ class Character : public Creature, public visitable int get_armor_acid( bodypart_id bp ) const; /** Returns overall resistance to given type on the bod part */ int get_armor_type( damage_type dt, bodypart_id bp ) const override; + std::map get_all_armor_type( damage_type dt, + const std::map> &clothing_map ) const; int get_stim() const; void set_stim( int new_stim ); @@ -1859,9 +1863,11 @@ class Character : public Creature, public visitable void set_destination_activity( const player_activity &new_destination_activity ); void clear_destination_activity(); /** Returns warmth provided by armor, etc. */ - int warmth( const bodypart_id &bp ) const; + std::map warmth( const std::map> + &clothing_map ) const; /** Returns warmth provided by an armor's bonus, like hoods, pockets, etc. */ - int bonus_item_warmth( const bodypart_id &bp ) const; + std::map bonus_item_warmth( const std::map> + &clothing_map ) const; /** Can the player lie down and cover self with blankets etc. **/ bool can_use_floor_warmth() const; /** diff --git a/src/game.cpp b/src/game.cpp index 67ea621878de6..828a5dba175df 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -1806,26 +1806,30 @@ int get_heat_radiation( const tripoint &location, bool direct ) // Stored as intensity-distance pairs int temp_mod = 0; int best_fire = 0; - for( const tripoint &dest : g->m.points_in_radius( location, 6 ) ) { + Character &player_character = get_avatar(); + map &here = get_map(); + // Convert it to an int id once, instead of 139 times per turn + const field_type_id fd_fire_int = fd_fire.id(); + for( const tripoint &dest : here.points_in_radius( location, 6 ) ) { int heat_intensity = 0; - maptile mt = g->m.maptile_at( dest ); + maptile mt = here.maptile_at( dest ); - int ffire = maptile_field_intensity( mt, fd_fire ); + int ffire = maptile_field_intensity( mt, fd_fire_int ); if( ffire > 0 ) { heat_intensity = ffire; - } else if( g->m.tr_at( dest ).loadid == tr_lava ) { - heat_intensity = 3; + } else { + heat_intensity = mt.get_ter()->heat_radiation; } if( heat_intensity == 0 ) { // No heat source here continue; } - if( g->u.pos() == location ) { - if( !g->m.pl_line_of_sight( dest, -1 ) ) { + if( player_character.pos() == location ) { + if( !here.pl_line_of_sight( dest, -1 ) ) { continue; } - } else if( !g->m.sees( location, dest, -1 ) ) { + } else if( !here.sees( location, dest, -1 ) ) { continue; } // Ensure fire_dist >= 1 to avoid divide-by-zero errors. diff --git a/src/item.cpp b/src/item.cpp index aa6ef440eefd9..c07434b63e24f 100644 --- a/src/item.cpp +++ b/src/item.cpp @@ -6348,6 +6348,30 @@ bool item::is_irremovable() const return has_flag( flag_IRREMOVABLE ); } +int item::wind_resist() const +{ + std::vector materials = made_of_types(); + if( materials.empty() ) { + debugmsg( "Called item::wind_resist on an item (%s) made of nothing!", tname() ); + return 99; + } + + int best = -1; + for( const material_type *mat : materials ) { + cata::optional resistance = mat->wind_resist(); + if( resistance && *resistance > best ) { + best = *resistance; + } + } + + // Default to 99% effective + if( best == -1 ) { + return 99; + } + + return best; +} + std::set item::faults_potential() const { std::set res; diff --git a/src/item.h b/src/item.h index 12f08e4e65b10..694e3202edf02 100644 --- a/src/item.h +++ b/src/item.h @@ -1158,6 +1158,9 @@ class item : public visitable item *get_food(); const item *get_food() const; + /** How resistant clothes made of this material are to wind (0-100) */ + int wind_resist() const; + /** What faults can potentially occur with this item? */ std::set faults_potential() const; diff --git a/src/mapdata.h b/src/mapdata.h index c3fdf80b9cc6c..795b8a0f6be59 100644 --- a/src/mapdata.h +++ b/src/mapdata.h @@ -348,6 +348,8 @@ struct ter_t : map_data_common_t { trap_id trap; // The id of the trap located at this terrain. Limit one trap per tile currently. + int heat_radiation = 0; // In fire field intensity "units" + ter_t(); static size_t count(); diff --git a/src/material.cpp b/src/material.cpp index 385f3d60e6675..54d4a7b442a5b 100644 --- a/src/material.cpp +++ b/src/material.cpp @@ -68,6 +68,7 @@ void material_type::load( const JsonObject &jsobj, const std::string & ) mandatory( jsobj, was_loaded, "density", _density ); optional( jsobj, was_loaded, "warmth_when_wet", _warmth_when_wet ); + optional( jsobj, was_loaded, "wind_resist", _wind_resist ); optional( jsobj, was_loaded, "specific_heat_liquid", _specific_heat_liquid ); optional( jsobj, was_loaded, "specific_heat_solid", _specific_heat_solid ); @@ -127,6 +128,11 @@ void material_type::check() const if( !item::type_is_defined( _repaired_with ) ) { debugmsg( "invalid \"repaired_with\" %s for %s.", _repaired_with.c_str(), id.c_str() ); } + + if( _wind_resist && ( *_wind_resist > 100 || *_wind_resist < 0 ) ) { + debugmsg( "Wind resistance outside of range (100%% to 0%%, is %d%%) for %s.", *_wind_resist, + id.str() ); + } for( auto &ca : _compact_accepts ) { if( !ca.is_valid() ) { debugmsg( "invalid \"compact_accepts\" %s for %s.", ca.c_str(), id.c_str() ); @@ -238,6 +244,11 @@ int material_type::density() const return _density; } +cata::optional material_type::wind_resist() const +{ + return _wind_resist; +} + bool material_type::edible() const { return _edible; diff --git a/src/material.h b/src/material.h index dda59cfb951ef..9d96d01f2be27 100644 --- a/src/material.h +++ b/src/material.h @@ -43,6 +43,8 @@ class material_type int _density = 1; // relative to "powder", which is 1 float _warmth_when_wet = 0.2f; // Percentage of warmth kept when fully drenched float _specific_heat_liquid = 4.186; + // How resistant this material is to wind as a percentage - 0 to 100 + cata::optional _wind_resist; float _specific_heat_solid = 2.108; float _latent_heat = 334; int _freeze_point = 32; // Fahrenheit @@ -99,6 +101,7 @@ class material_type float latent_heat() const; int freeze_point() const; int density() const; + cata::optional wind_resist() const; bool edible() const; bool rotting() const; bool soft() const; diff --git a/src/weather.cpp b/src/weather.cpp index 233e8305fb851..ac52bc69f38ba 100644 --- a/src/weather.cpp +++ b/src/weather.cpp @@ -369,18 +369,34 @@ static void fill_water_collectors( int mmPerHour, bool acid ) */ static void wet_player( int amount ) { + Character &target = get_avatar(); if( !is_player_outside() || - g->u.has_trait( trait_FEATHERS ) || - g->u.weapon.has_flag( "RAIN_PROTECT" ) || - ( !one_in( 50 ) && g->u.worn_with_flag( "RAINPROOF" ) ) ) { + target.has_trait( trait_FEATHERS ) || + target.weapon.has_flag( "RAIN_PROTECT" ) || + ( !one_in( 50 ) && target.worn_with_flag( "RAINPROOF" ) ) ) { return; } // Coarse correction to get us back to previously intended soaking rate. if( !calendar::once_every( 6_seconds ) ) { return; } - const int warmth_delay = g->u.warmth( bodypart_id( "torso" ) ) * 4 / 5 + g->u.warmth( - bodypart_id( "head" ) ) / 5; + std::map> clothing_map; + for( const bodypart_id &bp : target.get_all_body_parts() ) { + clothing_map.emplace( bp, std::vector() ); + } + for( const item &it : target.worn ) { + // TODO: Port body part set id changes + const body_part_set &covered = it.get_covered_body_parts(); + for( size_t i = 0; i < num_bp; i++ ) { + body_part token = static_cast( i ); + if( covered.test( token ) ) { + clothing_map[convert_bp( token )].emplace_back( &it ); + } + } + } + std::map warmth_bp = target.warmth( clothing_map ); + const int warmth_delay = warmth_bp[body_part_torso] * 0.8 + + warmth_bp[body_part_head] * 0.2; if( rng( 0, 100 - amount + warmth_delay ) > 10 ) { // Thick clothing slows down (but doesn't cap) soaking return; diff --git a/tests/player_test.cpp b/tests/player_test.cpp index 5c0bb42adc72e..bec39fd7932b5 100644 --- a/tests/player_test.cpp +++ b/tests/player_test.cpp @@ -19,6 +19,30 @@ #include "hash_utils.h" #include "overmapbuffer.h" +struct body_part_temp { + body_part_temp( bodypart_str_id part, int temperature ) + : part( part ), temperature( temperature ) + {} + bodypart_str_id part; + int temperature; + + bool operator==( const body_part_temp &other ) const { + return part == other.part && temperature == other.temperature; + } +}; + +namespace std +{ + +template<> struct hash { + std::size_t operator()( body_part_temp const &bpt ) const noexcept { + auto tuple_hash = cata::auto_hash>(); + return tuple_hash( std::forward_as_tuple( bpt.part, bpt.temperature ) ); + } +}; + +} // namespace std + struct temperature_threshold { constexpr temperature_threshold( int v, const char *n ) : value( v ) @@ -40,9 +64,25 @@ constexpr std::array bodytemps = {{ #undef t +std::ostream &operator<<( std::ostream &os, const body_part_temp &bpt ) +{ + // Stringify the temperature to avoid Catch adding hex + return os << std::to_string( bpt.temperature ); +} + +std::ostream &operator<<( std::ostream &os, const std::vector &bpts ) +{ + os << "["; + for( const auto &e : bpts ) { + os << e << ","; + } + return os << "]\n"; +} + // Run update_bodytemp() until core body temperature settles. static int converge_temperature( player &p, size_t iters, int start_temperature = BODYTEMP_NORM ) { + constexpr size_t n_history = 10; REQUIRE( get_weather().weather == WEATHER_CLOUDY ); REQUIRE( get_weather().windspeed == 0 ); @@ -55,29 +95,41 @@ static int converge_temperature( player &p, size_t iters, int start_temperature bool converged = false; - using temp_array = decltype( p.temp_cur ); - std::unordered_set history( iters ); + std::unordered_set, cata::range_hash> history( iters ); + std::list> last_n_history; + + const auto &parts = p.get_body(); for( size_t i = 0; i < iters; i++ ) { - if( history.count( p.temp_cur ) != 0 ) { + std::vector current_iter_temperature; + current_iter_temperature.reserve( parts.size() ); + for( const auto &pr : parts ) { + current_iter_temperature.emplace_back( pr.first, p.temp_cur[pr.first->token] ); + } + if( history.count( current_iter_temperature ) != 0 ) { converged = true; break; } - history.emplace( p.temp_cur ); + history.emplace( current_iter_temperature ); + last_n_history.emplace_front( current_iter_temperature ); + while( last_n_history.size() > n_history ) { + last_n_history.pop_back(); + } p.update_bodytemp( get_map(), g->weather ); } CAPTURE( iters ); CAPTURE( p.temp_cur ); + CAPTURE( last_n_history ); // If it doesn't converge, it's usually very close to it anyway, so don't fail CHECK( converged ); return p.temp_cur[0]; } -static void equip_clothing( player &p, const std::vector &clothing ) +static void equip_clothing( player &p, const std::vector &clothing ) { - for( const std::string &c : clothing ) { + for( const itype_id &c : clothing ) { const item article( c, calendar::start_of_cataclysm ); p.wear_item( article ); } @@ -110,7 +162,7 @@ static void test_temperature_spread( player &p, get_weather().clear_temp_cache(); CAPTURE( air_temperatures[i] ); CAPTURE( get_weather().temperature ); - int converged_temperature = converge_temperature( p, 1000, bodytemps[i].value ); + int converged_temperature = converge_temperature( p, 1500, bodytemps[i].value ); auto expected_body_temperature = bodytemps[i].name; CAPTURE( expected_body_temperature ); int air_temperature_celsius = to_celsius( air_temperatures[i] ); @@ -122,7 +174,7 @@ static void test_temperature_spread( player &p, } } -const std::vector light_clothing = {{ +const std::vector light_clothing = {{ "hat_ball", "bandana", "tshirt", @@ -133,7 +185,7 @@ const std::vector light_clothing = {{ } }; -const std::vector heavy_clothing = {{ +const std::vector heavy_clothing = {{ "hat_knit", "tshirt", "vest", @@ -146,7 +198,7 @@ const std::vector heavy_clothing = {{ } }; -const std::vector arctic_clothing = {{ +const std::vector arctic_clothing = {{ "balclava", "goggles_ski", "hat_hunting", @@ -231,8 +283,9 @@ static std::array find_temperature_points( for( int i = -200; i < 200; i++ ) { get_weather().temperature = i; get_weather().clear_temp_cache(); - CAPTURE( units::from_fahrenheit( i ) ); int converged_temperature = converge_temperature( p, 10000 ); + CAPTURE( i ); + CAPTURE( units::from_fahrenheit( i ) ); CHECK( converged_temperature >= last_converged_temperature ); // 0 - FREEZING, 6 - SCORCHING for( size_t temperature_index = 0; temperature_index < bodytemps.size(); temperature_index++ ) {