From fb0fa7558b1cab7ea10a3b96698151bbf46c8fc5 Mon Sep 17 00:00:00 2001 From: ShnitzelX2 <65314588+ShnitzelX2@users.noreply.github.com> Date: Fri, 25 Oct 2024 17:14:37 -0400 Subject: [PATCH] modify random NPC starting equipment based on time passed -some very basic item groups for now, needs to be expanded on later -add_profession_items(): replace raw inv with i_add() -very minor edits to some professions --- data/json/npcs/NC_NONE.json | 128 ++++++++++++++++++++++++ data/json/npcs/NC_NONE_HARDENED.json | 130 ++++++++++++++++++++++++ data/json/npcs/classes.json | 6 ++ data/json/professions.json | 11 +-- src/character.cpp | 22 +++++ src/character.h | 4 + src/newcharacter.cpp | 81 ++++++++++----- src/npc.cpp | 141 ++++++++++++++++++++++----- src/npc.h | 6 ++ 9 files changed, 476 insertions(+), 53 deletions(-) create mode 100644 data/json/npcs/NC_NONE.json create mode 100644 data/json/npcs/NC_NONE_HARDENED.json diff --git a/data/json/npcs/NC_NONE.json b/data/json/npcs/NC_NONE.json new file mode 100644 index 0000000000000..a362ded868ee6 --- /dev/null +++ b/data/json/npcs/NC_NONE.json @@ -0,0 +1,128 @@ +[ + { + "type": "item_group", + "id": "NC_NONE_foot", + "subtype": "distribution", + "groups": [ [ "clothing_outdoor_shoes", 50 ], [ "shoes", 25 ] ] + }, + { + "type": "item_group", + "id": "NC_NONE_arm", + "subtype": "distribution", + "entries": [ { "item": "null" } ] + }, + { + "type": "item_group", + "id": "NC_NONE_hand", + "subtype": "distribution", + "groups": [ [ "clothing_work_gloves", 25 ], [ "common_gloves", 25 ] ] + }, + { + "type": "item_group", + "id": "NC_NONE_head", + "subtype": "distribution", + "groups": [ [ "hatstore_hats", 50 ], [ "clothing_work_hat", 25 ] ] + }, + { + "type": "item_group", + "id": "NC_NONE_leg", + "subtype": "distribution", + "groups": [ [ "clothing_outdoor_pants", 50 ], [ "pants", 25 ] ] + }, + { + "type": "item_group", + "id": "NC_NONE_mouth", + "subtype": "distribution", + "groups": [ [ "hatstore_accessories", 50 ], [ "masks_unisex", 10 ] ] + }, + { + "type": "item_group", + "id": "NC_NONE_sensor", + "subtype": "distribution", + "groups": [ "clothing_glasses" ] + }, + { + "type": "item_group", + "id": "NC_NONE_torso", + "subtype": "distribution", + "groups": [ [ "clothing_outdoor_torso", 50 ], [ "jackets", 25 ], [ "shirts", 25 ] ] + }, + { + "type": "item_group", + "id": "NC_NONE_stabbing", + "subtype": "distribution", + "items": [ "knife_folding" ] + }, + { + "type": "item_group", + "id": "NC_NONE_bashing", + "items": [ + [ "hammer", 20 ], + [ "wrench", 20 ], + [ "hammer_sledge", 20 ], + [ "pipe", 20 ], + [ "baton-extended", 20 ], + [ "crowbar", 20 ] + ] + }, + { + "type": "item_group", + "id": "NC_NONE_cutting", + "subtype": "distribution", + "items": [ "knife_cleaver" ] + }, + { + "type": "item_group", + "id": "NC_NONE_throw", + "items": [ [ "throwing_knife", 50 ], [ "throwing_axe", 50 ] ] + }, + { + "type": "item_group", + "id": "NC_NONE_archery", + "subtype": "distribution", + "items": [ "crossbow" ] + }, + { + "type": "item_group", + "id": "NC_NONE_pistol", + "subtype": "distribution", + "groups": [ "guns_pistol_common" ] + }, + { + "type": "item_group", + "id": "NC_NONE_shotgun", + "subtype": "distribution", + "groups": [ "guns_shotgun_common" ] + }, + { + "type": "item_group", + "id": "NC_NONE_smg", + "subtype": "distribution", + "groups": [ "guns_smg_milspec" ] + }, + { + "type": "item_group", + "id": "NC_NONE_rifle", + "subtype": "distribution", + "groups": [ "guns_rifle_common" ] + }, + { + "type": "item_group", + "id": "NC_NONE_storage", + "subtype": "distribution", + "groups": [ "bags" ] + }, + { + "type": "item_group", + "id": "NC_NONE_extra", + "subtype": "distribution", + "groups": [ + [ "vending_drink_items", 70 ], + [ "cannedfood", 50 ], + [ "drugs_heal_simple", 10 ], + [ "big_canned_food", 10 ], + [ "produce", 5 ], + [ "harddrugs", 5 ] + ] + } +] diff --git a/data/json/npcs/NC_NONE_HARDENED.json b/data/json/npcs/NC_NONE_HARDENED.json new file mode 100644 index 0000000000000..649a449815609 --- /dev/null +++ b/data/json/npcs/NC_NONE_HARDENED.json @@ -0,0 +1,130 @@ +[ + { + "type": "item_group", + "id": "NC_NONE_HARDENED_foot", + "subtype": "distribution", + "groups": [ "survivorzed_boots" ] + }, + { + "type": "item_group", + "id": "NC_NONE_HARDENED_arm", + "subtype": "distribution", + "items": [ { "item": "null" } ] + }, + { + "type": "item_group", + "id": "NC_NONE_HARDENED_hand", + "subtype": "distribution", + "groups": [ "survivorzed_gloves" ] + }, + { + "type": "item_group", + "id": "NC_NONE_HARDENED_head", + "subtype": "distribution", + "groups": [ "survivorzed_head" ] + }, + { + "type": "item_group", + "id": "NC_NONE_HARDENED_leg", + "subtype": "distribution", + "groups": [ "survivorzed_bottoms" ] + }, + { + "type": "item_group", + "id": "NC_NONE_HARDENED_mouth", + "subtype": "distribution", + "groups": [ [ "hatstore_accessories", 50 ], [ "clothing_work_mask", 50 ] ] + }, + { + "type": "item_group", + "id": "NC_NONE_HARDENED_sensor", + "subtype": "distribution", + "groups": [ "clothing_work_glasses" ] + }, + { + "type": "item_group", + "id": "NC_NONE_HARDENED_torso", + "subtype": "distribution", + "entries": [ + { "group": "military_ballistic_vest", "prob": 25 }, + { "item": "kevlar", "prob": 25 }, + { "item": "chestguard_hard", "prob": 10 }, + { "group": "survivorzed_tops", "prob": 10 } + ] + }, + { + "type": "item_group", + "id": "NC_NONE_HARDENED_stabbing", + "subtype": "distribution", + "items": [ "glaive" ] + }, + { + "type": "item_group", + "id": "NC_NONE_HARDENED_bashing", + "subtype": "distribution", + "items": [ "bat_metal" ] + }, + { + "type": "item_group", + "id": "NC_NONE_HARDENED_cutting", + "subtype": "distribution", + "items": [ "sword_sheets_welded_large" ] + }, + { + "type": "item_group", + "id": "NC_NONE_HARDENED_throw", + "subtype": "distribution", + "entries": [ { "group": "NC_NONE_throw", "prob": 50 }, { "item": "javelin_fletched", "prob": 50 } ] + }, + { + "type": "item_group", + "id": "NC_NONE_HARDENED_archery", + "subtype": "distribution", + "items": [ "longbow" ] + }, + { + "type": "item_group", + "id": "NC_NONE_HARDENED_pistol", + "subtype": "distribution", + "groups": [ "guns_pistol_common" ] + }, + { + "type": "item_group", + "id": "NC_NONE_HARDENED_shotgun", + "subtype": "distribution", + "groups": [ "guns_shotgun_common" ] + }, + { + "type": "item_group", + "id": "NC_NONE_HARDENED_smg", + "subtype": "distribution", + "groups": [ "guns_smg_milspec" ] + }, + { + "type": "item_group", + "id": "NC_NONE_HARDENED_rifle", + "subtype": "distribution", + "groups": [ "guns_rifle_common" ] + }, + { + "type": "item_group", + "id": "NC_NONE_HARDENED_storage", + "subtype": "distribution", + "groups": [ "bags" ] + }, + { + "type": "item_group", + "id": "NC_NONE_HARDENED_extra", + "subtype": "distribution", + "groups": [ + [ "NC_NONE_extra", 50 ], + [ "electronics", 10 ], + [ "swat_gear", 5 ], + [ "gear_eod", 5 ], + [ "fireman_gear", 5 ], + [ "rad_gear", 5 ], + [ "military", 5 ], + [ "grenades", 5 ] + ] + } +] diff --git a/data/json/npcs/classes.json b/data/json/npcs/classes.json index 663a3685a6db4..153ba9815f5d0 100644 --- a/data/json/npcs/classes.json +++ b/data/json/npcs/classes.json @@ -9,6 +9,12 @@ { "skill": "ALL", "level": { "mul": [ { "one_in": 3 }, { "sum": [ { "dice": [ 4, 2 ] }, { "rng": [ -4, -1 ] } ] } ] } } ] }, + { + "type": "npc_class", + "id": "NC_NONE_HARDENED", + "name": { "str": "No class" }, + "job_description": "I'm a dummy class for better NPC items later into the Cataclysm." + }, { "type": "npc_class", "id": "NC_TEST_CLASS", diff --git a/data/json/professions.json b/data/json/professions.json index 542500ab43970..c1a3716486a29 100644 --- a/data/json/professions.json +++ b/data/json/professions.json @@ -495,7 +495,7 @@ { "type": "profession_item_substitutions", "item": "socks_wool", - "sub": [ { "present": [ "WOOLALLERGY", "VEGAN" ], "new": [ "socks" ] } ] + "sub": [ { "present": [ "VEGAN" ], "new": [ "socks" ] }, { "present": [ "WOOLALLERGY" ], "new": [ "socks" ] } ] }, { "type": "profession_item_substitutions", @@ -600,7 +600,7 @@ { "present": [ "ANTIWHEAT", "ANTIJUNK" ], "absent": [ "VEGETARIAN" ], - "new": [ { "item": "fish_fried", "ratio": 2 }, { "item": "fork", "ratio": 0.5 } ] + "new": [ { "item": "fish_fried", "ratio": 1 }, { "item": "fork", "ratio": 0.5 } ] } ] }, @@ -618,7 +618,7 @@ { "present": [ "ANTIWHEAT", "ANTIJUNK" ], "absent": [ "VEGETARIAN" ], - "new": [ { "item": "fish_fried", "ratio": 2 }, { "item": "fork", "ratio": 0.5 } ] + "new": [ { "item": "fish_fried", "ratio": 1 }, { "item": "fork", "ratio": 0.5 } ] }, { "present": [ "MEATARIAN" ], "absent": [ "ANTIWHEAT" ], "new": [ "pizza_meat" ] } ] @@ -1262,7 +1262,7 @@ { "item": "mouthpiece" }, { "item": "socks_ankle" }, { "item": "cleats" }, - { "item": "football" }, + { "item": "football", "custom-flags": [ "auto_wield" ] }, { "item": "knee_pads" }, { "item": "sports_drink" }, { "item": "jersey", "snippets": [ "endsville" ] }, @@ -4095,7 +4095,7 @@ { "item": "bra" }, { "item": "panties" }, { "item": "dress" }, - { "item": "purse" }, + { "item": "purse", "custom-flags": [ "auto_wield" ] }, { "item": "tourmaline_silver_bracelet" } ] } @@ -5153,7 +5153,6 @@ { "item": "jacket_light" }, { "item": "socks" }, { "item": "sneakers" }, - { "item": "pizza_veggy", "count": 4, "container-item": "null", "entry-wrapper": "box_small" }, { "item": "pizza_meat", "count": 4, "container-item": "null", "entry-wrapper": "box_small" }, { "item": "money_strap_one" }, { "item": "wristwatch" }, diff --git a/src/character.cpp b/src/character.cpp index 5c815a11f86be..6b73953f8da06 100644 --- a/src/character.cpp +++ b/src/character.cpp @@ -761,6 +761,28 @@ void Character::randomize_cosmetics() } } +void Character::starting_inv_damage_worn( int days ) +{ + //damage equipment depending on days passed + int chances_to_damage = std::max( days / 14 + rng( -3, 3 ), 0 ); + do { + std::vector worn_items; + worn.inv_dump( worn_items ); + if( !worn_items.empty() ) { + item *to_damage = random_entry( worn_items ); + int damage_count = rng( 1, 3 ); + bool destroy = false; + do { + destroy = to_damage->inc_damage(); + if( destroy ) { + //if the clothing was destroyed in a simulated "dangerous situation", all contained items are lost + i_rem( to_damage ); + } + } while( damage_count-- > 0 && !destroy ); + } + } while( chances_to_damage-- > 0 ); +} + field_type_id Character::bloodType() const { if( has_flag( json_flag_ACIDBLOOD ) ) { diff --git a/src/character.h b/src/character.h index f6d1084e79e5b..5772d56393dc9 100644 --- a/src/character.h +++ b/src/character.h @@ -1336,6 +1336,10 @@ class Character : public Creature, public visitable /** Returns the id of a random trait matching the given predicate */ trait_id get_random_trait( const std::function &func ); void randomize_cosmetic_trait( const std::string &mutation_type ); + /** Damages worn equipment + @param days - simulated number of in-game days passed + */ + void starting_inv_damage_worn( int days ); // In mutation.cpp /** Returns true if the player has a conflicting trait to the entered trait diff --git a/src/newcharacter.cpp b/src/newcharacter.cpp index 5822b679fa942..7566f0a01679b 100644 --- a/src/newcharacter.cpp +++ b/src/newcharacter.cpp @@ -604,10 +604,10 @@ void Character::randomize( const bool random_scenario, bool play_now ) reset_cardio_acc(); if( is_npc() ) { - add_profession_items(); as_npc()->randomize_personality(); as_npc()->generate_personality_traits(); initialize(); + add_profession_items(); as_npc()->catchup_skills(); } } @@ -620,39 +620,70 @@ void Character::add_profession_items() } std::list prof_items = prof->items( outfit, get_mutations() ); + std::list try_adding_again; - for( item &it : prof_items ) { - if( it.has_flag( STATIC( flag_id( "WET" ) ) ) ) { - it.active = true; - it.item_counter = 450; // Give it some time to dry off - } + auto attempt_add_items = [this]( std::list &prof_items, std::list &failed_to_add ) { + for( item &it : prof_items ) { + if( it.has_flag( STATIC( flag_id( "WET" ) ) ) ) { + it.active = true; + it.item_counter = 450; // Give it some time to dry off + } - // TODO: debugmsg if food that isn't a seed is inedible - if( it.has_flag( json_flag_no_auto_equip ) ) { - it.unset_flag( json_flag_no_auto_equip ); - inv->push_back( it ); - } else if( it.has_flag( json_flag_auto_wield ) ) { - it.unset_flag( json_flag_auto_wield ); - if( !has_wield_conflicts( it ) ) { - wield( it ); + item_location success; + item *wield_or_wear = nullptr; + // TODO: debugmsg if food that isn't a seed is inedible + if( it.has_flag( json_flag_no_auto_equip ) ) { + it.unset_flag( json_flag_no_auto_equip ); + success = try_add( it, nullptr, nullptr, false ); + } else if( it.has_flag( json_flag_auto_wield ) ) { + it.unset_flag( json_flag_auto_wield ); + if( !has_wield_conflicts( it ) ) { + wield( it ); + wield_or_wear = ⁢ + success = item_location( *this, wield_or_wear ); + } else { + success = try_add( it, nullptr, nullptr, false ); + } + } else if( it.is_armor() ) { + if( can_wear( it ).success() ) { + wear_item( it, false, false ); + wield_or_wear = ⁢ + success = item_location( *this, wield_or_wear ); + } else { + success = try_add( it, nullptr, nullptr, false ); + } } else { - inv->push_back( it ); + success = try_add( it, nullptr, nullptr, false ); } - } else if( it.is_armor() ) { - if( can_wear( it ).success() ) { - wear_item( it, false, false ); - } else { - inv->push_back( it ); + + if( it.is_book() && this->is_avatar() ) { + as_avatar()->identify( it ); + } + + if( !success ) { + failed_to_add.emplace_back( it ); } - } else { - inv->push_back( it ); } + }; - if( it.is_book() && this->is_avatar() ) { - as_avatar()->identify( it ); + //storage items may not be added first, so a second attempt is needed + attempt_add_items( prof_items, try_adding_again ); + prof_items.clear(); + attempt_add_items( try_adding_again, prof_items ); + //if there's one item left that still can't be added, attempt to wield it + if( prof_items.size() == 1 ) { + item last_item = prof_items.front(); + if( !has_wield_conflicts( last_item ) ) { + bool success_wield = wield( last_item ); + if( success_wield ) { + prof_items.pop_front(); + } } } - + if( !prof_items.empty() ) { + debugmsg( "Failed to place %d items in inventory for profession %s", prof_items.size(), + prof->gender_appropriate_name( male ) ); + } recalc_sight_limits(); calc_encumbrance(); } diff --git a/src/npc.cpp b/src/npc.cpp index 15e280ae60ca0..7db31ad331189 100644 --- a/src/npc.cpp +++ b/src/npc.cpp @@ -132,6 +132,7 @@ static const npc_class_id NC_BOUNTY_HUNTER( "NC_BOUNTY_HUNTER" ); static const npc_class_id NC_COWBOY( "NC_COWBOY" ); static const npc_class_id NC_EVAC_SHOPKEEP( "NC_EVAC_SHOPKEEP" ); static const npc_class_id NC_NONE( "NC_NONE" ); +static const npc_class_id NC_NONE_HARDENED( "NC_NONE_HARDENED" ); static const npc_class_id NC_TRADER( "NC_TRADER" ); static const overmap_location_str_id overmap_location_source_of_ammo( "source_of_ammo" ); @@ -171,6 +172,7 @@ class monfaction; static void starting_clothes( npc &who, const npc_class_id &type, bool male ); static void starting_inv( npc &who, const npc_class_id &type ); +static void starting_inv_ammo( npc &who, std::list &res, int multiplier ); bool job_data::set_task_priority( const activity_id &task, int new_priority ) { @@ -563,6 +565,7 @@ void npc::randomize( const npc_class_id &type, const npc_template_id &tem_id ) } if( type.is_null() || type == NC_NONE ) { Character::randomize( false ); + starting_inv_passtime(); return; } @@ -914,28 +917,10 @@ void starting_inv( npc &who, const npc_class_id &type ) return; } - // If wielding a gun, get some additional ammo for it - const item_location weapon = who.get_wielded_item(); - if( weapon && weapon->is_gun() ) { - item ammo; - if( !weapon->magazine_default().is_null() ) { - item mag( weapon->magazine_default() ); - mag.ammo_set( mag.ammo_default() ); - ammo = item( mag.ammo_default() ); - res.push_back( mag ); - } else if( !weapon->ammo_default().is_null() ) { - ammo = item( weapon->ammo_default() ); - // TODO: Move to npc_class - // NC_COWBOY and NC_BOUNTY_HUNTER get 5-15 whilst all others get 3-6 - int qty = 1 + ( type == NC_COWBOY || - type == NC_BOUNTY_HUNTER ); - qty = rng( qty, qty * 2 ); - - while( qty-- != 0 && who.can_stash( ammo ) ) { - res.push_back( ammo ); - } - } - } + // TODO: Move to npc_class + // NC_COWBOY and NC_BOUNTY_HUNTER get double + int multiplier = ( type == NC_COWBOY || type == NC_BOUNTY_HUNTER ) ? 2 : 1; + starting_inv_ammo( who, res, multiplier ); if( type == NC_ARSONIST ) { res.emplace_back( "molotov" ); @@ -966,6 +951,118 @@ void starting_inv( npc &who, const npc_class_id &type ) *who.inv += res; } +/** +Give npc ammo for ranged weapon +@param res - list of items to return +@param multiplier - magazine/ammo quantity multiplier +*/ +void starting_inv_ammo( npc &who, std::list &res, int multiplier ) +{ + // If wielding a gun, get some additional ammo for it + const item_location weapon = who.get_wielded_item(); + int ammo_quantity; + item ammo = item(); + ammo_quantity = rng( 1, 2 ) * multiplier; + if( weapon && weapon->is_gun() ) { + if( !weapon->magazine_default().is_null() ) { + ammo = item( weapon->magazine_default() ); + ammo.ammo_set( ammo.ammo_default() ); + } else if( !weapon->ammo_default().is_null() ) { + ammo = item( weapon->ammo_default() ); + } else { + return; + } + while( ammo_quantity-- != 0 && who.can_stash( ammo ) ) { + res.push_back( ammo ); + } + } +} + +void npc::starting_inv_passtime() +{ + static int max_time = to_days( 180_days ); + auto npc_wear_item = []( npc * who, item & it ) { + if( it.has_flag( flag_VARSIZE ) ) { + it.set_flag( flag_FIT ); + } + if( who->can_wear( it ).success() ) { + it.on_wear( *who ); + who->worn.wear_item( *who, it, false, false ); + it.set_owner( *who ); + } + }; + auto found_good_item = []( int day ) { + return ( x_in_y( day, max_time ) ? NC_NONE_HARDENED : NC_NONE ); + }; + + std::map starting_coverage; + for( const bodypart_id &part : get_all_body_parts() ) { + starting_coverage.emplace( part, worn.get_coverage( part ) ); + } + + int days_since_cata = std::min( to_days( calendar::turn - calendar::start_of_cataclysm ), + max_time ); + //give storage item if too little volume + if( worn.volume_capacity() < 10000_ml ) { + item storage = random_item_from( found_good_item( days_since_cata ), "storage" ); + npc_wear_item( this, storage ); + } + //damage worn starting equipment + starting_inv_damage_worn( days_since_cata ); + //replace equipment for basic coverage + for( const bodypart_id &part : get_all_body_parts() ) { + int cov = worn.get_coverage( part ); + if( cov < starting_coverage[part] || cov == 0 ) { + item clothing = random_item_from( found_good_item( days_since_cata ), + io::enum_to_string( part->primary_limb_type() ) ); + if( one_in( 2 ) ) { + clothing.inc_damage(); //lightly used equipment + } + npc_wear_item( this, clothing ); + } + } + //if no weapon on person, give one based on best weapon skill + std::vector items = all_items_loc(); + bool has_weapon = false; + for( const item_location &i : items ) { + if( i->is_melee() || i->is_gun() ) { + has_weapon = true; + break; + } + } + if( !has_weapon ) { + starting_weapon( NC_NONE ); + //additional ammo guaranteed if given a weapon + std::list res; + starting_inv_ammo( *this, res, 1 ); + for( item &ammo : res ) { + try_add( ammo, nullptr, nullptr, false ); + } + } + + //extra items if storage allows + int items_added = 0; + int items_limit = 2 + rng_normal( 4 * ( static_cast( std::min( days_since_cata, + max_time ) ) / static_cast( max_time ) ) ); + if( one_in( 16 ) ) { + items_limit += rng( 8, 12 ); + } else if( one_in( 16 ) ) { + items_limit = rng( 1, 2 ); + } + do { + item next_to_add = random_item_from( found_good_item( days_since_cata ), "extra" ); + if( can_stash( next_to_add ) ) { + if( !next_to_add.has_flag( flag_TRADER_AVOID ) ) { + next_to_add.set_owner( *this ); + try_add( next_to_add, nullptr, nullptr, false ); + } + } else { + break; + } + items_added++; + } while( items_added <= items_limit ); +} + void npc::revert_after_activity() { mission = previous_mission; diff --git a/src/npc.h b/src/npc.h index b264f11c98e0e..c04b6a20a4497 100644 --- a/src/npc.h +++ b/src/npc.h @@ -821,6 +821,12 @@ class npc : public Character void update_missions_target( character_id old_character, character_id new_character ); std::pair best_combat_skill( combat_skills subset ) const; void starting_weapon( const npc_class_id &type ); + /** + * Adds items to a randomly generated NPC (i.e. not having a defined npc_class) + * As time passes, NPCs get stronger (reaching peak at 90 days) + * See NC_NONE_*.json and NC_NONE_HARDENED_*.json for item selection + */ + void starting_inv_passtime(); // Save & load void deserialize( const JsonObject &data ) override;