From fcab0db51bdab9cd1ff50ed9f656c16c3c8f558b Mon Sep 17 00:00:00 2001 From: David Seguin Date: Sat, 23 Oct 2021 14:51:00 -0400 Subject: [PATCH] Proportional Material (#52369) * Read "material" as string tags or portioned objects * Proportional damage resistance per material * Use the highest "portion" material as the base * Track insertion order for std::map * Fix materials being accessed before loading mods * Additional proportional effects for materials Take the proportion of certain materials into account for: - Glass breaking (from smashing, striking, throwing, etc.) - Weapons can have up to 20% glass before not being able to counter - Magnetic pull is less effective for items with lower metal content --- doc/JSON_INFO.md | 17 +++- src/auto_pickup.cpp | 14 +-- src/consumption.cpp | 2 +- src/game.cpp | 4 +- src/handle_action.cpp | 10 ++- src/item.cpp | 182 +++++++++++++++++++++++--------------- src/item.h | 6 +- src/item_factory.cpp | 101 ++++++++++++++------- src/item_search.cpp | 4 +- src/itype.cpp | 2 +- src/itype.h | 9 +- src/iuse_actor.cpp | 24 +++-- src/map.cpp | 4 +- src/map_field.cpp | 14 +-- src/melee.cpp | 15 +++- src/monattack.cpp | 15 ++-- src/monstergenerator.cpp | 17 ++-- src/mtype.cpp | 8 +- src/mtype.h | 3 +- src/ranged.cpp | 13 ++- src/vehicle_move.cpp | 5 +- tests/iuse_actor_test.cpp | 11 ++- 22 files changed, 308 insertions(+), 172 deletions(-) diff --git a/doc/JSON_INFO.md b/doc/JSON_INFO.md index f47288f8f0d20..7056c73a07154 100644 --- a/doc/JSON_INFO.md +++ b/doc/JSON_INFO.md @@ -2376,7 +2376,10 @@ See also VEHICLE_JSON.md "insulation": 1, // (Optional, default = 1) If container or vehicle part, how much insulation should it provide to the contents "price": 100, // Used when bartering with NPCs. For stackable items (ammo, comestibles) this is the price for stack_size charges. Can use string "cent" "USD" or "kUSD". "price_postapoc": "1 USD", // Same as price but represent value post cataclysm. Can use string "cent" "USD" or "kUSD". -"material": ["COTTON"], // Material types, can be as many as you want. See materials.json for possible options +"material": [ // Material types, can be as many as you want. See materials.json for possible options + { "type": "cotton", "portion": 9 }, // type indicates the material's ID, portion indicates proportionally how much of the item is composed of that material + { "type": "plastic" } // portion can be omitted and will default to 1. In this case, the item is 90% cotton and 10% plastic. +], "weapon_category": [ "WEAPON_CAT1" ], // (Optional) Weapon categories this item is in for martial arts. "cutting": 0, // (Optional, default = 0) Cutting damage caused by using it as a melee weapon. This value cannot be negative. "bashing": 0, // (Optional, default = 0) Bashing damage caused by using it as a melee weapon. This value cannot be negative. @@ -2768,7 +2771,10 @@ CBMs can be defined like this: "parasites": 10, // (Optional) Probability of becoming parasitised when eating "contamination": [ { "disease": "bad_food", "probability": 5 } ], // (Optional) List of diseases carried by this comestible and their associated probability. Values must be in the [0, 100] range. "vitamins": [ [ "calcium", 5 ], [ "iron", 12 ] ], // Vitamins provided by consuming a charge (portion) of this. An integer percentage of ideal daily value average. Vitamins array keys include the following: calcium, iron, vitA, vitB, vitC, mutant_toxin, bad_food, blood, and redcells. Note that vitB is B12. -"material": [ "flesh", "wheat" ], // All materials (IDs) this food is made of +"material": [ // All materials (IDs) this food is made of + { "type": "flesh", "portion": 3 }, // See Generic Item attributes for type and portion details + { "type": "wheat", "portion": 5 } +], "primary_material": "meat", // What the primary material ID is. Materials determine specific heat. "rot_spawn": "MONSTERGROUP_NAME", // Monster group that spawns when food becomes rotten (used for egg hatching) "rot_spawn_chance": 10, // Percent chance of monstergroup spawn when food rots. Max 100. @@ -2819,7 +2825,10 @@ Any Item can be a container. To add the ability to contain things to an item, yo "name": "hatchet", // In-game name displayed "description": "A one-handed hatchet. Makes a great melee weapon, and is useful both for cutting wood, and for use as a hammer.", // In-game description "price": 95, // Used when bartering with NPCs. Can use string "cent" "USD" or "kUSD". -"material": ["iron", "wood"], // Material types. See materials.json for possible options +"material": [ // Material types. See materials.json for possible options + { "type": "iron", "portion": 2 }, // See Generic Item attributes for type and portion details + { "type": "wood", "portion": 3 } +], "weight": 907, // Weight, measured in grams "volume": "1500 ml", // Volume, volume in ml and L can be used - "50 ml" or "2 L" "bashing": 12, // Bashing damage caused by using it as a melee weapon @@ -2933,7 +2942,7 @@ Alternately, every item (book, tool, armor, even food) can be used as a gunmod i "name": "torch (lit)", // In-game name displayed "description": "A large stick, wrapped in gasoline soaked rags. This is burning, producing plenty of light", // In-game description "price": 0, // Used when bartering with NPCs. Can use string "cent" "USD" or "kUSD". -"material": [ "wood" ], // Material types. See materials.json for possible options +"material": [ { "type": "wood", "portion": 1 } ], // Material types. See materials.json for possible options. Also see Generic Item attributes for type and portion details "techniques": [ "FLAMING" ], // Combat techniques used by this tool "flags": [ "FIRE" ], // Indicates special effects "weight": 831, // Weight, measured in grams diff --git a/src/auto_pickup.cpp b/src/auto_pickup.cpp index 97360e90bb7eb..1ecb9a49f1ec2 100644 --- a/src/auto_pickup.cpp +++ b/src/auto_pickup.cpp @@ -30,7 +30,7 @@ using namespace auto_pickup; -static bool check_special_rule( const std::vector &materials, +static bool check_special_rule( const std::map &materials, const std::string &rule ); auto_pickup::player_settings &get_auto_pickup() @@ -610,7 +610,7 @@ bool player_settings::empty() const return global_rules.empty() && character_rules.empty(); } -bool check_special_rule( const std::vector &materials, const std::string &rule ) +bool check_special_rule( const std::map &materials, const std::string &rule ) { char type = ' '; std::vector filter; @@ -624,16 +624,18 @@ bool check_special_rule( const std::vector &materials, const std::s } if( type == 'm' ) { - return std::any_of( materials.begin(), materials.end(), [&filter]( const material_id & mat ) { + return std::any_of( materials.begin(), + materials.end(), [&filter]( const std::pair &mat ) { return std::any_of( filter.begin(), filter.end(), [&mat]( const std::string & search ) { - return lcmatch( mat->name(), search ); + return lcmatch( mat.first->name(), search ); } ); } ); } else if( type == 'M' ) { - return std::all_of( materials.begin(), materials.end(), [&filter]( const material_id & mat ) { + return std::all_of( materials.begin(), + materials.end(), [&filter]( const std::pair &mat ) { return std::any_of( filter.begin(), filter.end(), [&mat]( const std::string & search ) { - return lcmatch( mat->name(), search ); + return lcmatch( mat.first->name(), search ); } ); } ); } diff --git a/src/consumption.cpp b/src/consumption.cpp index b1a911aac2a09..653641715c96b 100644 --- a/src/consumption.cpp +++ b/src/consumption.cpp @@ -672,7 +672,7 @@ ret_val Character::can_eat( const item &food ) const if( edible || drinkable ) { for( const auto &elem : food.type->materials ) { - if( !elem->edible() ) { + if( !elem.first->edible() ) { return ret_val::make_failure( _( "That doesn't look edible in its current form." ) ); } } diff --git a/src/game.cpp b/src/game.cpp index 7f5210bf4114f..34b2e1af37641 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -4749,7 +4749,9 @@ bool game::forced_door_closing( const tripoint &p, const ter_id &door_type, int it = items.erase( it ); continue; } - if( it->made_of( material_id( "glass" ) ) && one_in( 2 ) ) { + const int glass_portion = it->made_of( material_id( "glass" ) ); + const float glass_fraction = glass_portion / static_cast( it->type->mat_portion_total ); + if( glass_portion && rng_float( 0.0f, 1.0f ) < glass_fraction * 0.5f ) { if( can_see ) { add_msg( m_warning, _( "A %s shatters!" ), it->tname() ); } else { diff --git a/src/handle_action.cpp b/src/handle_action.cpp index 91e590d05e230..fa127a3ab413e 100644 --- a/src/handle_action.cpp +++ b/src/handle_action.cpp @@ -809,9 +809,13 @@ static void smash() if( player_character.get_skill_level( skill_melee ) == 0 ) { player_character.practice( skill_melee, rng( 0, 1 ) * rng( 0, 1 ) ); } - const int vol = weapon.volume() / units::legacy_volume_factor; - if( weapon.made_of( material_id( "glass" ) ) && - rng( 0, vol + 3 ) < vol ) { + const int glass_portion = weapon.made_of( material_id( "glass" ) ); + float glass_fraction = glass_portion / static_cast( weapon.type->mat_portion_total ); + if( std::isnan( glass_fraction ) || glass_fraction > 1.f ) { + glass_fraction = 0.f; + } + const int vol = weapon.volume() * glass_fraction / units::legacy_volume_factor; + if( glass_portion && rng( 0, vol + 3 ) < vol ) { add_msg( m_bad, _( "Your %s shatters!" ), weapon.tname() ); weapon.spill_contents( player_character.pos() ); sounds::sound( player_character.pos(), 24, sounds::sound_t::combat, "CRACK!", true, "smash", diff --git a/src/item.cpp b/src/item.cpp index 45f401ec18e13..84c75ab35f6ec 100644 --- a/src/item.cpp +++ b/src/item.cpp @@ -6867,13 +6867,14 @@ float item::bash_resist( bool to_self ) const const float eff_damage = to_self ? std::min( dmg, 0 ) : std::max( dmg, 0 ); const float eff_thickness = std::max( 0.1f, get_thickness() - eff_damage ); - const std::vector mat_types = made_of_types(); - if( !mat_types.empty() ) { - for( const material_type *mat : mat_types ) { - resist += mat->bash_resist(); + const int total = type->mat_portion_total == 0 ? 1 : type->mat_portion_total; + const std::map mats = made_of(); + if( !mats.empty() ) { + for( const auto &m : mats ) { + resist += m.first->bash_resist() * m.second; } - // Average based on number of materials. - resist /= mat_types.size(); + // Average based portion of materials + resist /= total; } return ( resist * eff_thickness ) + mod; @@ -6895,13 +6896,14 @@ float item::cut_resist( bool to_self ) const const float eff_damage = to_self ? std::min( dmg, 0 ) : std::max( dmg, 0 ); const float eff_thickness = std::max( 0.1f, base_thickness - eff_damage ); - const std::vector mat_types = made_of_types(); - if( !mat_types.empty() ) { - for( const material_type *mat : mat_types ) { - resist += mat->cut_resist(); + const int total = type->mat_portion_total == 0 ? 1 : type->mat_portion_total; + const std::map mats = made_of(); + if( !mats.empty() ) { + for( const auto &m : mats ) { + resist += m.first->cut_resist() * m.second; } - // Average based on number of materials. - resist /= mat_types.size(); + // Average based portion of materials + resist /= total; } return ( resist * eff_thickness ) + mod; @@ -6933,13 +6935,14 @@ float item::bullet_resist( bool to_self ) const const float eff_damage = to_self ? std::min( dmg, 0 ) : std::max( dmg, 0 ); const float eff_thickness = std::max( 0.1f, base_thickness - eff_damage ); - const std::vector mat_types = made_of_types(); - if( !mat_types.empty() ) { - for( const material_type *mat : mat_types ) { - resist += mat->bullet_resist(); + const int total = type->mat_portion_total == 0 ? 1 : type->mat_portion_total; + const std::map mats = made_of(); + if( !mats.empty() ) { + for( const auto &m : mats ) { + resist += m.first->bullet_resist() * m.second; } - // Average based on number of materials. - resist /= mat_types.size(); + // Average based portion of materials + resist /= total; } return ( resist * eff_thickness ) + mod; @@ -6958,16 +6961,16 @@ float item::acid_resist( bool to_self, int base_env_resist ) const return 0.0f; } - const std::vector mat_types = made_of_types(); - if( !mat_types.empty() ) { + const int total = type->mat_portion_total == 0 ? 1 : type->mat_portion_total; + const std::map mats = made_of(); + if( !mats.empty() ) { // Not sure why cut and bash get an armor thickness bonus but acid doesn't, // but such is the way of the code. - - for( const material_type *mat : mat_types ) { - resist += mat->acid_resist(); + for( const auto &m : mats ) { + resist += m.first->acid_resist() * m.second; } - // Average based on number of materials. - resist /= mat_types.size(); + // Average based portion of materials + resist /= total; } const int env = get_env_resist( base_env_resist ); @@ -6992,13 +6995,14 @@ float item::fire_resist( bool to_self, int base_env_resist ) const return 0.0f; } - const std::vector mat_types = made_of_types(); - if( !mat_types.empty() ) { - for( const material_type *mat : mat_types ) { - resist += mat->fire_resist(); + const std::map mats = made_of(); + const int total = type->mat_portion_total == 0 ? 1 : type->mat_portion_total; + if( !mats.empty() ) { + for( const auto &m : mats ) { + resist += m.first->fire_resist() * m.second; } - // Average based on number of materials. - resist /= mat_types.size(); + // Average based portion of materials + resist /= total; } const int env = get_env_resist( base_env_resist ); @@ -7013,8 +7017,9 @@ float item::fire_resist( bool to_self, int base_env_resist ) const int item::chip_resistance( bool worst ) const { int res = worst ? INT_MAX : INT_MIN; - for( const material_type *mat : made_of_types() ) { - const int val = mat->chip_resist(); + const int total = type->mat_portion_total == 0 ? 1 : type->mat_portion_total; + for( const std::pair mat : made_of() ) { + const int val = ( mat.first->chip_resist() * mat.second ) / total; res = worst ? std::min( res, val ) : std::max( res, val ); } @@ -7231,7 +7236,7 @@ bool item::is_two_handed( const Character &guy ) const return ( ( weight() / 113_gram ) > guy.str_cur * 4 ); } -const std::vector &item::made_of() const +const std::map &item::made_of() const { if( is_corpse() ) { return corpse->mat; @@ -7247,40 +7252,52 @@ const std::map &item::quality_of() const std::vector item::made_of_types() const { std::vector material_types_composed_of; - for( const material_id &mat_id : made_of() ) { - material_types_composed_of.push_back( &mat_id.obj() ); + if( is_corpse() ) { + for( const auto &mat_id : made_of() ) { + material_types_composed_of.push_back( &mat_id.first.obj() ); + } + } else { + for( const material_id &mat_id : type->mats_ordered ) { + material_types_composed_of.push_back( &mat_id.obj() ); + } } return material_types_composed_of; } bool item::made_of_any( const std::set &mat_idents ) const { - const std::vector &mats = made_of(); + const std::map &mats = made_of(); if( mats.empty() ) { return false; } - return std::any_of( mats.begin(), mats.end(), [&mat_idents]( const material_id & e ) { - return mat_idents.count( e ); + return std::any_of( mats.begin(), + mats.end(), [&mat_idents]( const std::pair &e ) { + return mat_idents.count( e.first ); } ); } bool item::only_made_of( const std::set &mat_idents ) const { - const std::vector &mats = made_of(); + const std::map &mats = made_of(); if( mats.empty() ) { return false; } - return std::all_of( mats.begin(), mats.end(), [&mat_idents]( const material_id & e ) { - return mat_idents.count( e ); + return std::all_of( mats.begin(), + mats.end(), [&mat_idents]( const std::pair &e ) { + return mat_idents.count( e.first ); } ); } -bool item::made_of( const material_id &mat_ident ) const +int item::made_of( const material_id &mat_ident ) const { - const std::vector &materials = made_of(); - return std::find( materials.begin(), materials.end(), mat_ident ) != materials.end(); + const std::map &materials = made_of(); + auto mat = materials.find( mat_ident ); + if( mat == materials.end() ) { + return 0; + } + return mat->second; } bool item::made_of( phase_id phase ) const @@ -7918,9 +7935,9 @@ bool item::is_salvageable() const if( is_null() ) { return false; } - const std::vector &mats = made_of(); - if( std::none_of( mats.begin(), mats.end(), [this]( const material_id & m ) { - return m->salvaged_into().has_value() && m->salvaged_into().value() != type->get_id(); + const std::map &mats = made_of(); + if( std::none_of( mats.begin(), mats.end(), [this]( const std::pair &m ) { + return m.first->salvaged_into().has_value() && m.first->salvaged_into().value() != type->get_id(); } ) ) { return false; } @@ -8204,13 +8221,31 @@ bool item::eipc_recipe_add( const recipe_id &recipe_id ) const material_type &item::get_random_material() const { - return random_entry( made_of(), material_id::NULL_ID() ).obj(); + std::vector matlist; + const std::map &mats = made_of(); + matlist.reserve( mats.size() ); + for( auto mat : mats ) { + matlist.emplace_back( mat.first ); + } + return *random_entry( matlist, material_id::NULL_ID() ); } const material_type &item::get_base_material() const { - const std::vector &mats = made_of(); - return mats.empty() ? material_id::NULL_ID().obj() : mats.front().obj(); + const std::map &mats = made_of(); + const material_type *m = &material_id::NULL_ID().obj(); + int portion = 0; + for( const std::pair mat : mats ) { + if( mat.second > portion ) { + portion = mat.second; + m = &mat.first.obj(); + } + } + // Material portions all equal / not specified. Select first material. + if( portion == 1 ) { + return *type->mats_ordered[0]; + } + return *m; } bool item::operator<( const item &other ) const @@ -9293,14 +9328,14 @@ bool item::reload( Character &u, item_location ammo, int qty ) float item::simulate_burn( fire_data &frd ) const { - const std::vector &mats = made_of(); + const std::map &mats = made_of(); float smoke_added = 0.0f; float time_added = 0.0f; float burn_added = 0.0f; const units::volume vol = base_volume(); const int effective_intensity = frd.contained ? 3 : frd.fire_intensity; - for( const material_id &m : mats ) { - const mat_burn_data &bd = m.obj().burn_data( effective_intensity ); + for( const auto &m : mats ) { + const mat_burn_data &bd = m.first->burn_data( effective_intensity ); if( bd.immune ) { // Made to protect from fire return 0.0f; @@ -9308,25 +9343,26 @@ float item::simulate_burn( fire_data &frd ) const // If fire is contained, burn rate is independent of volume if( frd.contained || bd.volume_per_turn == 0_ml ) { - time_added += bd.fuel; - smoke_added += bd.smoke; - burn_added += bd.burn; + time_added += bd.fuel * m.second; + smoke_added += bd.smoke * m.second; + burn_added += bd.burn * m.second; } else { double volume_burn_rate = to_liter( bd.volume_per_turn ) / to_liter( vol ); - time_added += bd.fuel * volume_burn_rate; - smoke_added += bd.smoke * volume_burn_rate; - burn_added += bd.burn * volume_burn_rate; + time_added += bd.fuel * volume_burn_rate * m.second; + smoke_added += bd.smoke * volume_burn_rate * m.second; + burn_added += bd.burn * volume_burn_rate * m.second; } } + const int mat_total = type->mat_portion_total == 0 ? 1 : type->mat_portion_total; // Liquids that don't burn well smother fire well instead if( made_of( phase_id::LIQUID ) && time_added < 200 ) { time_added -= rng( 400.0 * to_liter( vol ), 1200.0 * to_liter( vol ) ); } else if( mats.size() > 1 ) { // Average the materials - time_added /= mats.size(); - smoke_added /= mats.size(); - burn_added /= mats.size(); + time_added /= mat_total; + smoke_added /= mat_total; + burn_added /= mat_total; } else if( mats.empty() ) { // Non-liquid items with no specified materials will burn at moderate speed burn_added = 1; @@ -9387,7 +9423,7 @@ bool item::burn( fire_data &frd ) bool item::flammable( int threshold ) const { - const std::vector &mats = made_of_types(); + const std::map &mats = made_of(); if( mats.empty() ) { // Don't know how to burn down something made of nothing. return false; @@ -9395,22 +9431,24 @@ bool item::flammable( int threshold ) const int flammability = 0; units::volume volume_per_turn = 0_ml; - for( const material_type *m : mats ) { - const mat_burn_data &bd = m->burn_data( 1 ); + for( const std::pair m : mats ) { + const mat_burn_data &bd = m.first->burn_data( 1 ); if( bd.immune ) { // Made to protect from fire return false; } - flammability += bd.fuel; - volume_per_turn += bd.volume_per_turn; + flammability += bd.fuel * m.second; + volume_per_turn += bd.volume_per_turn * m.second; } + const int total = type->mat_portion_total == 0 ? 1 : type->mat_portion_total; + flammability /= total; + volume_per_turn /= total; if( threshold == 0 || flammability <= 0 ) { return flammability > 0; } - volume_per_turn /= mats.size(); units::volume vol = base_volume(); if( volume_per_turn > 0_ml && volume_per_turn < vol ) { flammability = flammability * volume_per_turn / vol; @@ -11154,9 +11192,9 @@ bool item::is_soft() const return false; } - const std::vector mats = made_of(); - return std::all_of( mats.begin(), mats.end(), []( const material_id & mid ) { - return mid.obj().soft(); + const std::map mats = made_of(); + return std::all_of( mats.begin(), mats.end(), []( const std::pair &mid ) { + return mid.first->soft(); } ); } diff --git a/src/item.h b/src/item.h index ddb531646a4b5..7ef70aa56aa9f 100644 --- a/src/item.h +++ b/src/item.h @@ -1024,6 +1024,7 @@ class item : public visitable const material_type &get_random_material() const; /** * Get the basic (main) material of this item. May return the null-material. + * This is the material with the highest "portion" value. */ const material_type &get_base_material() const; /** @@ -1031,7 +1032,7 @@ class item : public visitable * This may return an empty vector. * The returned vector does not contain the null id. */ - const std::vector &made_of() const; + const std::map &made_of() const; /** * The ids of all the qualities this contains. */ @@ -1055,8 +1056,9 @@ class item : public visitable /** * Check we are made of this material (e.g. matches at least one * in our set.) + * @return The portion of this item made up by the material */ - bool made_of( const material_id &mat_ident ) const; + int made_of( const material_id &mat_ident ) const; /** * Are we solid, liquid, gas, plasma? */ diff --git a/src/item_factory.cpp b/src/item_factory.cpp index c1650fd4398a0..ef8cbf824f18e 100644 --- a/src/item_factory.cpp +++ b/src/item_factory.cpp @@ -272,8 +272,8 @@ void Item_factory::finalize_pre( itype &obj ) } const auto &mats = obj.materials; - if( std::find( mats.begin(), mats.end(), material_id( "hydrocarbons" ) ) == mats.end() && - std::find( mats.begin(), mats.end(), material_id( "oil" ) ) == mats.end() ) { + if( mats.find( material_id( "hydrocarbons" ) ) == mats.end() && + mats.find( material_id( "oil" ) ) == mats.end() ) { const auto &ammo_effects = obj.ammo->ammo_effects; obj.ammo->cookoff = ammo_effects.count( "INCENDIARY" ) > 0 || ammo_effects.count( "COOKOFF" ) > 0; @@ -475,15 +475,19 @@ void Item_factory::finalize_pre( itype &obj ) auto mat = obj.materials; // TODO: migrate inedible comestibles to appropriate alternative types. - mat.erase( std::remove_if( mat.begin(), mat.end(), []( const string_id &m ) { - return !m.obj().edible(); - } ), mat.end() ); + for( auto m = mat.begin(); m != mat.end(); ) { + if( !m->first->edible() ) { + m = mat.erase( m ); + } else { + m++; + } + } // For comestibles composed of multiple edible materials we calculate the average. for( const auto &v : vitamin::all() ) { if( !vitamins.count( v.first ) ) { for( const auto &m : mat ) { - double amount = m->vitamin( v.first ) * healthy / mat.size(); + double amount = m.first->vitamin( v.first ) * healthy / mat.size(); vitamins[v.first] += std::ceil( amount ); } } @@ -579,8 +583,9 @@ void Item_factory::finalize_post( itype &obj ) // tool has a possible repair action, check if the materials are compatible const auto &opts = dynamic_cast( func->get_actor_ptr() )->materials; - if( std::any_of( obj.materials.begin(), obj.materials.end(), [&opts]( const material_id & m ) { - return opts.count( m ) > 0; + if( std::any_of( obj.materials.begin(), + obj.materials.end(), [&opts]( const std::pair &m ) { + return opts.count( m.first ) > 0; } ) ) { obj.repair.insert( tool ); } @@ -1299,9 +1304,9 @@ void Item_factory::check_definitions() const msg += "empty description\n"; } - for( const material_id &mat_id : type->materials ) { - if( mat_id.str() == "null" || !mat_id.is_valid() ) { - msg += string_format( "invalid material %s\n", mat_id.c_str() ); + for( const std::pair &mat_id : type->materials ) { + if( mat_id.first.str() == "null" || !mat_id.first.is_valid() ) { + msg += string_format( "invalid material %s\n", mat_id.first.c_str() ); } } @@ -2225,37 +2230,44 @@ void Item_factory::load( islot_comestible &slot, const JsonObject &jo, const std jsobj.get_int( "probability" ) ); } + bool is_not_boring = false; if( jo.has_member( "primary_material" ) ) { std::string mat = jo.get_string( "primary_material" ); slot.specific_heat_solid = material_id( mat )->specific_heat_solid(); slot.specific_heat_liquid = material_id( mat )->specific_heat_liquid(); slot.latent_heat = material_id( mat )->latent_heat(); + is_not_boring = is_not_boring || mat == "junk"; } else if( jo.has_member( "material" ) ) { float specific_heat_solid = 0.0f; float specific_heat_liquid = 0.0f; float latent_heat = 0.0f; + int mat_total = 0; + + auto add_spi = [&]( const material_id & m, int portion ) { + specific_heat_solid += m->specific_heat_solid() * portion; + specific_heat_liquid += m->specific_heat_liquid() * portion; + latent_heat += m->latent_heat() * portion; + mat_total += portion; + is_not_boring = is_not_boring || m == material_id( "junk" ); + }; - for( const std::string &m : jo.get_tags( "material" ) ) { - specific_heat_solid += material_id( m )->specific_heat_solid(); - specific_heat_liquid += material_id( m )->specific_heat_liquid(); - latent_heat += material_id( m )->latent_heat(); + if( jo.has_array( "material" ) && jo.get_array( "material" ).test_object() ) { + for( JsonObject m : jo.get_array( "material" ) ) { + const material_id mat_id( m.get_string( "type" ) ); + int portion = m.get_int( "portion", 1 ); + add_spi( mat_id, portion ); + } + } else { + for( const std::string &m : jo.get_tags( "material" ) ) { + add_spi( material_id( m ), 1 ); + } } // Average based on number of materials. - slot.specific_heat_liquid = specific_heat_liquid / jo.get_tags( "material" ).size(); - slot.specific_heat_solid = specific_heat_solid / jo.get_tags( "material" ).size(); - slot.latent_heat = latent_heat / jo.get_tags( "material" ).size(); + slot.specific_heat_liquid = specific_heat_liquid / mat_total; + slot.specific_heat_solid = specific_heat_solid / mat_total; + slot.latent_heat = latent_heat / mat_total; } - bool is_not_boring = false; - if( jo.has_member( "primary_material" ) ) { - std::string mat = jo.get_string( "primary_material" ); - is_not_boring = is_not_boring || mat == "junk"; - } - if( jo.has_member( "material" ) ) { - for( const std::string &m : jo.get_tags( "material" ) ) { - is_not_boring = is_not_boring || m == "junk"; - } - } // Junk food never gets old by default, but this can still be overridden. if( is_not_boring ) { slot.monotony_penalty = 0; @@ -2535,7 +2547,7 @@ void Item_factory::set_allergy_flags( itype &item_template ) const auto &mats = item_template.materials; for( const auto &pr : all_pairs ) { - if( std::find( mats.begin(), mats.end(), pr.first ) != mats.end() ) { + if( mats.find( pr.first ) != mats.end() ) { item_template.item_tags.insert( pr.second ); } } @@ -2547,11 +2559,19 @@ void hflesh_to_flesh( itype &item_template ) { auto &mats = item_template.materials; const size_t old_size = mats.size(); - mats.erase( std::remove( mats.begin(), mats.end(), material_id( "hflesh" ) ), mats.end() ); + int ports = 0; + for( auto mat = mats.begin(); mat != mats.end(); ) { + if( mat->first == material_id( "hflesh" ) ) { + ports += mat->second; + mat = mats.erase( mat ); + } else { + mat++; + } + } // Only add "flesh" material if not already present if( old_size != mats.size() && - std::find( mats.begin(), mats.end(), material_id( "flesh" ) ) == mats.end() ) { - mats.emplace_back( "flesh" ); + mats.find( material_id( "flesh" ) ) == mats.end() ) { + mats.emplace( "flesh", ports ); } } @@ -2973,8 +2993,21 @@ void Item_factory::load_basic_info( const JsonObject &jo, itype &def, const std: if( jo.has_member( "material" ) ) { def.materials.clear(); - for( const std::string &m : jo.get_tags( "material" ) ) { - def.materials.emplace_back( m ); + auto add_mat = [&def]( const material_id & m, int portion ) { + const auto res = def.materials.emplace( m, portion ); + if( res.second ) { + def.mats_ordered.emplace_back( m ); + def.mat_portion_total += portion; + } + }; + if( jo.has_array( "material" ) && jo.get_array( "material" ).test_object() ) { + for( JsonObject mat : jo.get_array( "material" ) ) { + add_mat( material_id( mat.get_string( "type" ) ), mat.get_int( "portion", 1 ) ); + } + } else { + for( const std::string &mat : jo.get_tags( "material" ) ) { + add_mat( material_id( mat ), 1 ); + } } } diff --git a/src/item_search.cpp b/src/item_search.cpp index 8f8d7c8ca0758..4b58b8051b07c 100644 --- a/src/item_search.cpp +++ b/src/item_search.cpp @@ -33,8 +33,8 @@ std::function basic_item_filter( std::string filter ) case 'm': return [filter]( const item & i ) { return std::any_of( i.made_of().begin(), i.made_of().end(), - [&filter]( const material_id & mat ) { - return lcmatch( mat->name(), filter ); + [&filter]( const std::pair &mat ) { + return lcmatch( mat.first->name(), filter ); } ); }; // qualities diff --git a/src/itype.cpp b/src/itype.cpp index 066d48af762c5..ca16ca39366f8 100644 --- a/src/itype.cpp +++ b/src/itype.cpp @@ -185,7 +185,7 @@ bool itype::can_have_charges() const bool itype::is_basic_component() const { for( const auto &mat : materials ) { - if( mat->salvaged_into() && *mat->salvaged_into() == get_id() ) { + if( mat.first->salvaged_into() && *mat.first->salvaged_into() == get_id() ) { return true; } } diff --git a/src/itype.h b/src/itype.h index ac84b62a88681..d59c7f2be1028 100644 --- a/src/itype.h +++ b/src/itype.h @@ -932,8 +932,15 @@ struct itype { std::vector conditional_names; // What we're made of (material names). .size() == made of nothing. + // First -> the material + // Second -> the portion of item covered by the material (portion / total portions) // MATERIALS WORK IN PROGRESS. - std::vector materials; + std::map materials; + // Since the material list was converted to a map, keep track of the material insert order + // Do not use this for materials. Use the materials map above. + std::vector mats_ordered; + // Total of item's material portions (materials->second) + int mat_portion_total = 0; /** Actions an instance can perform (if any) indexed by action type */ std::map use_methods; diff --git a/src/iuse_actor.cpp b/src/iuse_actor.cpp index 92a83814a7aa4..c0347cbce0ab5 100644 --- a/src/iuse_actor.cpp +++ b/src/iuse_actor.cpp @@ -1397,8 +1397,8 @@ cata::optional salvage_actor::use( Character &p, item &it, bool t, const tr // Helper to visit instances of all the sub-materials of an item. static void visit_salvage_products( const item &it, std::function func ) { - for( const material_id &material : it.made_of() ) { - if( const cata::optional id = material->salvaged_into() ) { + for( const auto &material : it.made_of() ) { + if( const cata::optional id = material.first->salvaged_into() ) { item exemplar( *id ); func( exemplar ); } @@ -1503,7 +1503,7 @@ bool salvage_actor::try_to_cut_up( Character &p, item &it ) const // cut gets cut int salvage_actor::cut_up( Character &p, item &it, item_location &cut ) const { - std::vector cut_material_components = cut.get_item()->made_of(); + const std::map cut_material_components = cut.get_item()->made_of(); const bool filthy = cut.get_item()->is_filthy(); float remaining_weight = 1; @@ -1562,15 +1562,20 @@ int salvage_actor::cut_up( Character &p, item &it, item_location &cut ) const continue; } // Discard invalid component - if( !temp.made_of_any( std::set( cut_material_components.begin(), - cut_material_components.end() ) ) ) { + std::set mat_set; + for( std::pair mat : cut_material_components ) { + mat_set.insert( mat.first ); + } + if( !temp.made_of_any( mat_set ) ) { continue; } //items count by charges should be even smaller than base materials if( !temp.is_salvageable() || temp.count_by_charges() ) { + const float mat_total = temp.type->mat_portion_total == 0 ? 1 : temp.type->mat_portion_total; // non-salvageable items but made of appropriate material, disrtibute uniformly in to all materials for( const auto &type : temp.made_of() ) { - mat_to_weight[type] += ( temp.weight() * remaining_weight / temp.made_of().size() ); + mat_to_weight[type.first] += ( temp.weight() * remaining_weight / temp.made_of().size() ) * + ( static_cast( type.second ) / mat_total ); } continue; } @@ -1602,11 +1607,13 @@ int salvage_actor::cut_up( Character &p, item &it, item_location &cut ) const // No crafting recipe available if( iter == recipe_dict.end() ) { // Check disassemble recipe too + const float mat_total = temp.type->mat_portion_total == 0 ? 1 : temp.type->mat_portion_total; un_craft = recipe_dictionary::get_uncraft( temp.typeId() ); if( un_craft.is_null() ) { // No recipes found, count weight and go next for( const auto &type : temp.made_of() ) { - mat_to_weight[type] += ( temp.weight() * remaining_weight / temp.made_of().size() ); + mat_to_weight[type.first] += ( temp.weight() * remaining_weight / temp.made_of().size() ) * + ( static_cast( type.second ) / mat_total ); } continue; } @@ -1618,7 +1625,8 @@ int salvage_actor::cut_up( Character &p, item &it, item_location &cut ) const if( weight > temp.weight() ) { // Bad disassemble recipe. Count weight and go next for( const auto &type : temp.made_of() ) { - mat_to_weight[type] += ( temp.weight() * remaining_weight / temp.made_of().size() ); + mat_to_weight[type.first] += ( temp.weight() * remaining_weight / temp.made_of().size() ) * + ( static_cast( type.second ) / mat_total ); } continue; } diff --git a/src/map.cpp b/src/map.cpp index 758f3251308f2..e0a495801d494 100644 --- a/src/map.cpp +++ b/src/map.cpp @@ -3556,7 +3556,9 @@ void map::bash_items( const tripoint &p, bash_params ¶ms ) bool smashed_glass = false; for( auto bashed_item = bashed_items.begin(); bashed_item != bashed_items.end(); ) { // the check for active suppresses Molotovs smashing themselves with their own explosion - if( bashed_item->made_of( material_id( "glass" ) ) && !bashed_item->active && one_in( 2 ) ) { + int glass_portion = bashed_item->made_of( material_id( "glass" ) ); + float glass_fraction = glass_portion / static_cast( bashed_item->type->mat_portion_total ); + if( glass_portion && !bashed_item->active && rng_float( 0.0f, 1.0f ) < glass_fraction * 0.5f ) { params.did_bash = true; smashed_glass = true; for( const item *bashed_content : bashed_item->all_items_top() ) { diff --git a/src/map_field.cpp b/src/map_field.cpp index eef32989b3151..59e0fafabffbc 100644 --- a/src/map_field.cpp +++ b/src/map_field.cpp @@ -96,14 +96,14 @@ using namespace map_field_processing; void map::create_burnproducts( const tripoint &p, const item &fuel, const units::mass &burned_mass ) { - std::vector all_mats = fuel.made_of(); + const std::map all_mats = fuel.made_of(); if( all_mats.empty() ) { return; } - // Items that are multiple materials are assumed to be equal parts each. - const units::mass by_weight = burned_mass / all_mats.size(); - for( material_id &mat : all_mats ) { - for( const auto &bp : mat->burn_products() ) { + const units::mass by_weight = burned_mass; + const float mat_total = fuel.type->mat_portion_total == 0 ? 1 : fuel.type->mat_portion_total; + for( const auto &mat : all_mats ) { + for( const auto &bp : mat.first->burn_products() ) { itype_id id = bp.first; // Spawning the same item as the one that was just burned is pointless // and leads to infinite recursion. @@ -111,7 +111,9 @@ void map::create_burnproducts( const tripoint &p, const item &fuel, const units: continue; } const float eff = bp.second; - const int n = std::floor( eff * ( by_weight / item::find_type( id )->weight ) ); + // distribute byproducts by weight AND portion of item + const int n = std::floor( eff * ( by_weight / item::find_type( id )->weight ) * + ( mat.second / mat_total ) ); if( n <= 0 ) { continue; diff --git a/src/melee.cpp b/src/melee.cpp index 459d9c0d3b46e..dc2b85af6a945 100644 --- a/src/melee.cpp +++ b/src/melee.cpp @@ -2037,9 +2037,10 @@ bool Character::block_hit( Creature *source, bodypart_id &bp_hit, damage_instanc matec_id tec = pick_technique( *source, shield, false, false, true ); if( tec != tec_none && !is_dead_state() ) { + int twenty_percent = std::round( ( 20 * weapon.type->mat_portion_total ) / 100.0f ); if( get_stamina() < get_stamina_max() / 3 ) { add_msg( m_bad, _( "You try to counterattack but you are too exhausted!" ) ); - } else if( weapon.made_of( material_id( "glass" ) ) ) { + } else if( weapon.made_of( material_id( "glass" ) ) > twenty_percent ) { add_msg( m_bad, _( "The item you are wielding is too fragile to counterattack with!" ) ); } else { melee_attack( *source, false, tec ); @@ -2128,11 +2129,17 @@ std::string Character::melee_special_effects( Creature &t, damage_instance &d, i } } - const int vol = weap.volume() / 250_ml; // Glass weapons shatter sometimes - if( weap.made_of( material_id( "glass" ) ) && + const int glass_portion = weap.made_of( material_id( "glass" ) ); + float glass_fraction = glass_portion / static_cast( weap.type->mat_portion_total ); + if( std::isnan( glass_fraction ) || glass_fraction > 1.f ) { + glass_fraction = 0.f; + } + // only consider portion of weapon made of glass + const int vol = weap.volume() * glass_fraction / 250_ml; + if( glass_portion && /** @EFFECT_STR increases chance of breaking glass weapons (NEGATIVE) */ - rng( 0, vol + 8 ) < vol + str_cur ) { + rng_float( 0.0f, vol + 8 ) < vol + str_cur ) { if( is_avatar() ) { dump += string_format( _( "Your %s shatters!" ), weap.tname() ) + "\n"; } else { diff --git a/src/monattack.cpp b/src/monattack.cpp index cb96a75b46766..4711f56c5c2f8 100644 --- a/src/monattack.cpp +++ b/src/monattack.cpp @@ -797,11 +797,13 @@ bool mattack::pull_metal_weapon( monster *z ) if( foe != nullptr ) { // Wielded steel or iron items except for built-in things like bionic claws or monomolecular blade const item &weapon = foe->get_wielded_item(); - if( !weapon.has_flag( flag_NO_UNWIELD ) && - ( weapon.made_of( material_id( "iron" ) ) || - weapon.made_of( material_id( "hardsteel" ) ) || - weapon.made_of( material_id( "steel" ) ) || - weapon.made_of( material_id( "budget_steel" ) ) ) ) { + const int metal_portion = weapon.made_of( material_id( "iron" ) ) + + weapon.made_of( material_id( "hardsteel" ) ) + + weapon.made_of( material_id( "steel" ) ) + + weapon.made_of( material_id( "budget_steel" ) ); + // Take the total portion of metal in the item into account + const float metal_fraction = metal_portion / static_cast( weapon.type->mat_portion_total ); + if( !weapon.has_flag( flag_NO_UNWIELD ) && metal_portion ) { const int wp_skill = foe->get_skill_level( skill_melee ); // It takes a while z->moves -= att_cost_pull; @@ -809,7 +811,8 @@ bool mattack::pull_metal_weapon( monster *z ) ///\EFFECT_STR increases resistance to pull_metal_weapon special attack if( foe->str_cur > min_str ) { ///\EFFECT_MELEE increases resistance to pull_metal_weapon special attack - success = std::max( 100 - ( 6 * ( foe->str_cur - 6 ) ) - ( 6 * wp_skill ), 0 ); + success = std::max( ( 100 * metal_fraction ) - ( 6 * ( foe->str_cur - 6 ) ) - ( 6 * wp_skill ), + 0.0f ); } game_message_type m_type = foe->is_avatar() ? m_bad : m_neutral; if( rng( 1, 100 ) <= success ) { diff --git a/src/monstergenerator.cpp b/src/monstergenerator.cpp index 95f3fca8cd3a0..e110928445750 100644 --- a/src/monstergenerator.cpp +++ b/src/monstergenerator.cpp @@ -657,9 +657,16 @@ void mtype::load( const JsonObject &jo, const std::string &src ) assign( jo, "ascii_picture", picture_id ); - optional( jo, was_loaded, "material", mat, string_id_reader<::material_type> {} ); + if( jo.has_member( "material" ) ) { + mat.clear(); + for( const std::string &m : jo.get_tags( "material" ) ) { + mat.emplace( m, 1 ); + mat_portion_total += 1; + } + } if( mat.empty() ) { // Assign a default "flesh" material to prevent crash (#48988) - mat.emplace_back( material_id( "flesh" ) ); + mat.emplace( material_id( "flesh" ), 1 ); + mat_portion_total += 1; } optional( jo, was_loaded, "species", species, string_id_reader<::species_type> {} ); optional( jo, was_loaded, "categories", categories, auto_flags_reader<> {} ); @@ -1266,9 +1273,9 @@ void MonsterGenerator::check_monster_definitions() const debugmsg( "monster %s has unknown death drop item group: %s", mon.id.c_str(), mon.death_drops.c_str() ); } - for( const material_id &m : mon.mat ) { - if( m.str() == "null" || !m.is_valid() ) { - debugmsg( "monster %s has unknown material: %s", mon.id.c_str(), m.c_str() ); + for( const auto &m : mon.mat ) { + if( m.first.str() == "null" || !m.first.is_valid() ) { + debugmsg( "monster %s has unknown material: %s", mon.id.c_str(), m.first.c_str() ); } } if( !mon.revert_to_itype.is_empty() && !item::type_is_defined( mon.revert_to_itype ) ) { diff --git a/src/mtype.cpp b/src/mtype.cpp index e1506822ca0a1..048d551341825 100644 --- a/src/mtype.cpp +++ b/src/mtype.cpp @@ -36,7 +36,7 @@ mtype::mtype() size = creature_size::medium; volume = 62499_ml; weight = 81499_gram; - mat = { material_id( "flesh" ) }; + mat = { { material_id( "flesh" ), 1 } }; phase = phase_id::SOLID; def_chance = 0; upgrades = false; @@ -89,7 +89,7 @@ void mtype::set_flag( m_flag flag, bool state ) bool mtype::made_of( const material_id &material ) const { - return std::find( mat.begin(), mat.end(), material ) != mat.end(); + return mat.find( material ) != mat.end(); } bool mtype::made_of_any( const std::set &materials ) const @@ -98,8 +98,8 @@ bool mtype::made_of_any( const std::set &materials ) const return false; } - return std::any_of( mat.begin(), mat.end(), [&materials]( const material_id & e ) { - return materials.count( e ); + return std::any_of( mat.begin(), mat.end(), [&materials]( const std::pair &e ) { + return materials.count( e.first ); } ); } diff --git a/src/mtype.h b/src/mtype.h index 24d969a752c54..eb13a71dbfc0b 100644 --- a/src/mtype.h +++ b/src/mtype.h @@ -287,7 +287,8 @@ struct mtype { std::set species; std::set categories; - std::vector mat; + std::map mat; + int mat_portion_total = 0; /** UTF-8 encoded symbol, should be exactly one cell wide. */ std::string sym; /** hint for tilesets that don't have a tile for this monster */ diff --git a/src/ranged.cpp b/src/ranged.cpp index 973e452f9cfa4..96ecbff1f7647 100644 --- a/src/ranged.cpp +++ b/src/ranged.cpp @@ -1029,11 +1029,13 @@ projectile Character::thrown_item_projectile( const item &thrown ) const int Character::thrown_item_total_damage_raw( const item &thrown ) const { projectile proj = thrown_item_projectile( thrown ); - const units::volume volume = thrown.volume(); proj.impact.add_damage( damage_type::BASH, std::min( thrown.weight() / 100.0_gram, static_cast( thrown_item_adjusted_damage( thrown ) ) ) ); + const int glass_portion = thrown.made_of( material_id( "glass" ) ); + const float glass_fraction = glass_portion / static_cast( thrown.type->mat_portion_total ); + const units::volume volume = thrown.volume() * glass_fraction; // Item will shatter upon landing, destroying the item, dealing damage, and making noise - if( !thrown.active && thrown.made_of( material_id( "glass" ) ) && + if( !thrown.active && glass_portion && rng( 0, units::to_milliliter( 2_liter - volume ) ) < get_str() * 100 ) { proj.impact.add_damage( damage_type::CUT, units::to_milliliter( volume ) / 500.0f ); } @@ -1087,9 +1089,12 @@ dealt_projectile_attack Character::throw_item( const tripoint &target, const ite } // Item will shatter upon landing, destroying the item, dealing damage, and making noise + const int glass_portion = thrown.made_of( material_id( "glass" ) ); + const float glass_fraction = glass_portion / static_cast( thrown.type->mat_portion_total ); + const units::volume glass_vol = volume * glass_fraction; /** @EFFECT_STR increases chance of shattering thrown glass items (NEGATIVE) */ - const bool shatter = !thrown.active && thrown.made_of( material_id( "glass" ) ) && - rng( 0, units::to_milliliter( 2_liter - volume ) ) < get_str() * 100; + const bool shatter = !thrown.active && glass_portion && + rng( 0, units::to_milliliter( 2_liter - glass_vol ) ) < get_str() * 100; // Item will burst upon landing, destroying the item, and spilling its contents const bool burst = thrown.has_property( "burst_when_filled" ) && thrown.is_container() && diff --git a/src/vehicle_move.cpp b/src/vehicle_move.cpp index c8958a99e4c1f..638df4f6af1a5 100644 --- a/src/vehicle_move.cpp +++ b/src/vehicle_move.cpp @@ -907,10 +907,11 @@ veh_collision vehicle::part_collision( int part, const tripoint &p, //Calculate damage resulting from d_E const itype *type = item::find_type( part_info( ret.part ).base_item ); const auto &mats = type->materials; + float mat_total = type->mat_portion_total == 0 ? 1 : type->mat_portion_total; float vpart_dens = 0.0f; if( !mats.empty() ) { - for( const material_id &mat_id : mats ) { - vpart_dens += mat_id.obj().density(); + for( const std::pair &mat_id : mats ) { + vpart_dens += mat_id.first->density() * ( static_cast( mat_id.second ) / mat_total ); } // average vpart_dens /= mats.size(); diff --git a/tests/iuse_actor_test.cpp b/tests/iuse_actor_test.cpp index f622455304bd5..1794a290f63f7 100644 --- a/tests/iuse_actor_test.cpp +++ b/tests/iuse_actor_test.cpp @@ -130,11 +130,14 @@ static void cut_up_yields( const std::string &target ) salvage_actor test_actor; item cut_up_target{ target }; item tool{ "knife_butcher" }; - const std::vector &target_materials = cut_up_target.made_of(); + const std::map &target_materials = cut_up_target.made_of(); + const float mat_total = cut_up_target.type->mat_portion_total == 0 ? 1 : + cut_up_target.type->mat_portion_total; units::mass smallest_yield_mass = units::mass_max; - for( const material_id &mater : target_materials ) { - if( const cata::optional item_id = mater->salvaged_into() ) { - smallest_yield_mass = std::min( smallest_yield_mass, item_id->obj().weight ); + for( const auto &mater : target_materials ) { + if( const cata::optional item_id = mater.first->salvaged_into() ) { + units::mass portioned_weight = item_id->obj().weight * ( mater.second / mat_total ); + smallest_yield_mass = std::min( smallest_yield_mass, portioned_weight ); } } REQUIRE( smallest_yield_mass != units::mass_max );