diff --git a/data/json/effects.json b/data/json/effects.json index 82e8a4fe8e1bd..e8e9fb871171d 100644 --- a/data/json/effects.json +++ b/data/json/effects.json @@ -226,6 +226,12 @@ "desc": [ "AI tag used for screecher sounds. This is a bug if you have it." ], "show_in_info": true }, + { + "type": "effect_type", + "id": "looting", + "name": [ "Monster is looting" ], + "desc": [ "AI tag used for monster looting. This is a bug if you have it." ] + }, { "type": "effect_type", "id": "targeted", diff --git a/doc/JSON_FLAGS.md b/doc/JSON_FLAGS.md index 85f597177209d..ba76b5701765b 100644 --- a/doc/JSON_FLAGS.md +++ b/doc/JSON_FLAGS.md @@ -914,6 +914,7 @@ Multiple death functions can be used. Not all combinations make sense. - ```DOGFOOD``` Becomes friendly / tamed with dog food. - ```DRIPS_GASOLINE``` Occasionally drips gasoline on move. - ```DRIPS_NAPALM``` Occasionally drips napalm on move. +- ```EATS_FOOD``` Occasionally will eat food items in inventory and lootable food items nearby - ```ELECTRIC``` Shocks unarmed attackers. - ```ELECTRONIC``` e.g. A Robot; affected by emp blasts and other stuff. - ```FAT``` May produce fat when butchered. diff --git a/doc/MONSTERS.md b/doc/MONSTERS.md index b7a0185bd6d39..1a9c1272a7493 100644 --- a/doc/MONSTERS.md +++ b/doc/MONSTERS.md @@ -309,6 +309,48 @@ An object containing ammo that newly spawned monsters start with. This is useful "starting_ammo": { "9mm": 100, "40mm_frag": 100 } ``` +## "loots" +(object, optional) + +An object containing items that the monster will attempt to loot nearby. Example: +```JSON +"loots": { "item_group": [ "rings_and_things" ], "materials": [ "gold", "diamond" ], "categories": [ "artifacts" ], "requires_all": false, "paths_to": true } +``` + +The above example would allow the monster to actively attempt to loot any item found within the item group 'rings and things' OR any item made of gold or diamond OR any item that falls within the artificats category. + +The loots object may have the following members: + +### "requires_all" +(boolean) + +If true, requires that all types set in members "materials", "comestible_type" and "categories" match the item. + +### "paths_to" +(boolean) + +If false, monster will not move to loot an item. Useful for monsters that don't move much or wander. + +### "item_group" +(array of strings, optional) + +Item groups this monster will attempt to loot, for example "rings_and_things" + +### "materials" +(array of strings, optional) + +Material types of items this mosnter will attempt to loot, for example: "gold" + +### "comestible_type" +(array of strings, optional) + +Comestible types this monster will attempt to loot, for example: "FOOD" or "MED" + +### "categories" +(array of strings, optional) + +Item categories this monster will attempt to loot, for example: "spare_parts" + ## "upgrades" (boolean or object, optional) diff --git a/src/cata_string_consts.h b/src/cata_string_consts.h index bf4167fc72301..6a4f0260fe622 100644 --- a/src/cata_string_consts.h +++ b/src/cata_string_consts.h @@ -95,6 +95,7 @@ static const efftype_id effect_jetinjector( "jetinjector" ); static const efftype_id effect_lack_sleep( "lack_sleep" ); static const efftype_id effect_laserlocked( "laserlocked" ); static const efftype_id effect_lightsnare( "lightsnare" ); +static const efftype_id effect_looting("looting"); static const efftype_id effect_lying_down( "lying_down" ); static const efftype_id effect_melatonin_supplements( "melatonin" ); static const efftype_id effect_masked_scent( "masked_scent" ); diff --git a/src/monmove.cpp b/src/monmove.cpp index 74d1fd3ec1feb..b2047260c4946 100644 --- a/src/monmove.cpp +++ b/src/monmove.cpp @@ -43,12 +43,22 @@ #include "string_formatter.h" #include "cata_string_consts.h" + +//Related to monster loot +#include "item.h" +#include "itype.h" +#include "item_location.h" +#include "item_category.h" +#include "item_group.h" + static const species_id FUNGUS( "FUNGUS" ); static const species_id INSECT( "INSECT" ); static const species_id SPIDER( "SPIDER" ); static const species_id ZOMBIE( "ZOMBIE" ); + #define MONSTER_FOLLOW_DIST 8 +#define MONSTER_LOOT_DIST 16 bool monster::wander() { @@ -277,6 +287,123 @@ float monster::rate_target( Creature &c, float best, bool smart ) const return FLT_MAX; } +template +bool monster::is_item_lootable( const item &itm, const L &list, const P &predicate, + bool requires_all ) +{ + bool allowed = false; + + for( auto tag : list ) { + if( predicate( itm, tag ) ) { + allowed = true; + continue; + } else { + if( requires_all ) { + allowed = false; + break; + } + } + } + + return allowed; +} + +std::map monster::find_loot_in_radius( const tripoint &target, int radius ) +{ + std::map lootable_inrad; + + for( const tripoint &p : g->m.points_in_radius( target, radius ) ) { + if( g->m.sees_some_items( p, *this ) ) { + auto items = g->m.i_at( p ); + + for( item &itm : items ) { + bool suitable = false; + bool cat_suitable = is_item_lootable( itm, lootable.categories, []( const item & itm, + const std::string & tag ) { + return itm.get_category().name() == tag; + }, lootable.requires_all ); + bool mat_suitable = is_item_lootable( itm, lootable.materials, []( const item & itm, + const material_id & tag ) { + return itm.made_of( tag ); + }, lootable.requires_all ); + bool com_suitable = is_item_lootable( itm, lootable.comestibles, []( const item & itm, + const std::string & tag ) { + return itm.is_comestible() && itm.get_comestible()->comesttype == tag; + }, lootable.requires_all ); + bool grp_suitable = is_item_lootable( itm, lootable.itemsgroups, []( const item & itm, + const std::string & tag ) { + return item_group::group_contains_item( tag, itm.typeId() ); + }, false ); + + if( lootable.requires_all && ( cat_suitable && mat_suitable && com_suitable && grp_suitable ) ) { + suitable = true; + } + + if( !lootable.requires_all && ( cat_suitable || mat_suitable || com_suitable || grp_suitable ) ) { + suitable = true; + } + + if( suitable ) { + lootable_inrad.insert( std::make_pair( &itm, p ) ); + } + } + } + } + + return lootable_inrad; +} + +bool monster::eat_from_inventory( int amount ) +{ + auto it = inv.begin(); + for( item &itm : inv ) { + //Is food? We make a broad assumption here that anything the monster has in its inventory and... + // is considered food is a viable item to eat. Perhaps some monsters would consume items instead. + if( itm.is_comestible() && itm.get_comestible()->comesttype == "FOOD" ) { + if( itm.charges < amount ) { + continue; + } + + if( g->u.sees( *this ) ) { + add_msg( m_warning, _( "%1$s eats %2$s!" ), name(), itm.display_name() ); + } + + if( itm.charges > amount ) { + itm.mod_charges( -amount ); + } else { + inv.erase( it ); + } + + mod_moves( -50 ); + return true; + } + + it++; + } + + return false; +} + +item_location monster::select_desired_loot( std::map &loot ) +{ + std::map::iterator it; + item *desired_i = nullptr; + tripoint desired_p; + int desired_range = -1; + + for( it = loot.begin(); it != loot.end(); it++ ) { + int d = rl_dist( pos(), it->second ); + + if( d < desired_range || desired_range == -1 ) { + desired_i = it->first; + desired_p = it->second; + desired_range = d; + } + } + + return item_location( map_cursor( desired_p ), desired_i ); +} + void monster::plan() { const auto &factions = g->critter_tracker->factions(); @@ -350,6 +477,26 @@ void monster::plan() } } + //Check if steals, must be idle and have no target in sight. + if( ( friendly >= 0 || target == nullptr ) && !fleeing && !has_effect( effect_looting ) ) { + //If stealing monster, inventory is empty and with a bit of luck...we search for loot. + if( lootable.loots && inv.empty() && one_in( 100 ) ) { + //Find all lootable items within radius + std::map lootable_items = find_loot_in_radius( pos(), + lootable.paths_to ? MONSTER_LOOT_DIST : 1 ); + + if( !lootable_items.empty() ) { + item_goal = select_desired_loot( lootable_items ); + set_dest( item_goal.position() ); + add_effect( effect_looting, 100_turns ); + } + } else if( !inv.empty() && one_in( 150 ) ) { + if( has_flag( MF_EATS_FOOD ) ) { + eat_from_inventory(); + } + } + } + if( docile ) { if( friendly != 0 && target != nullptr ) { set_dest( target->pos() ); @@ -923,6 +1070,7 @@ void monster::move() } } } + const bool can_open_doors = has_flag( MF_CAN_OPEN_DOORS ); // Finished logic section. By this point, we should have chosen a square to // move to (moved = true). @@ -932,6 +1080,7 @@ void monster::move() ( !pacified && attack_at( local_next_step ) ) || ( !pacified && can_open_doors && g->m.open_door( local_next_step, !g->m.is_outside( pos() ) ) ) || ( !pacified && bash_at( local_next_step ) ) || + ( !pacified && lootable.loots && pickup_at( local_next_step, item_goal ) ) || ( !pacified && push_to( local_next_step, 0, 0 ) ) || move_to( local_next_step, false, false, get_stagger_adjust( pos(), destination, local_next_step ) ); @@ -1402,6 +1551,71 @@ bool monster::attack_at( const tripoint &p ) return false; } +bool monster::pickup_at( const tripoint &p, item_location &target ) +{ + //Whether we succeed or not, let's remove the effect. + if( has_effect( effect_looting ) ) { + remove_effect( effect_looting ); + } + + if( p != goal ) { + return false; + } + + if( target.get_item() == nullptr ) { + return false; + } + + int charges_to_pick = target->charges; + + if( charges_to_pick <= 0 ) { + return false; + } + + int charges_picked; + + //Get weight of the stack or item + units::mass weight = target->weight( true ); + units::mass capacity = this->weight_capacity(); + + //Adjust volume and weight for units in a stack + units::mass weight_each = weight / charges_to_pick; + charges_picked = std::min( target->charges, int( capacity / weight_each ) ); + + //Caps the amount taken to avoid taking large stacks. + //TODO: Allow this to be adjusted in JSON + charges_picked = std::min( charges_picked, 2 ); + + //Add item to inventory and adjust charges + inv.push_back( *target ); + target->mod_charges( -charges_picked ); + inv.back().charges = charges_picked; + + //If we've taken all the stack, let's remove the item + if( charges_picked == charges_to_pick ) { + target.remove_item(); + } + + //Successfully taken any? + if( charges_picked >= 1 ) { + //If the player can see us and we dont eat food, then grab the item and notify the player... + //...eat_from_inventory( charges_picked ); notifies the player in its own method. + if( g->u.sees( *this ) && !has_flag( MF_EATS_FOOD ) ) { + add_msg( m_warning, _( "%1$s grabs %2$s!" ), name(), inv.back().display_name() ); + } else { + return eat_from_inventory( charges_picked ); + } + + mod_moves( -100 ); + return true; + } + + //Failed to take any items, so remove the last item added to inventory. This is unlikely to happen. + inv.pop_back(); + add_msg( m_warning, _( "%1$s fails to grab %2$s!" ), name(), target->display_name() ); + return false; +} + static tripoint find_closest_stair( const tripoint &near_this, const ter_bitflags stair_type ) { for( const tripoint &candidate : closest_tripoints_first( near_this, 10 ) ) { diff --git a/src/monster.cpp b/src/monster.cpp index fcc0305862636..3c3e7046f75ea 100644 --- a/src/monster.cpp +++ b/src/monster.cpp @@ -140,6 +140,7 @@ static const std::map> attitu {monster_attitude::MATT_IGNORE, {translate_marker( "Ignoring." ), def_c_light_gray}}, {monster_attitude::MATT_ZLAVE, {translate_marker( "Zombie slave." ), def_c_green}}, {monster_attitude::MATT_ATTACK, {translate_marker( "Hostile!" ), def_c_red}}, + {monster_attitude::MATT_LOOT, {translate_marker( "Looting!" ), def_c_blue}}, {monster_attitude::MATT_NULL, {translate_marker( "BUG: Behavior unnamed." ), def_h_red}}, }; @@ -199,6 +200,8 @@ monster::monster( const mtype_id &id ) : monster() mech_bat_item.ammo_consume( rng( 0, max_charge ), tripoint_zero ); battery_item = cata::make_value( mech_bat_item ); } + + lootable = type->loot; } monster::monster( const mtype_id &id, const tripoint &p ) : monster( id ) @@ -611,6 +614,10 @@ int monster::print_info( const catacurses::window &w, int vStart, int vLines, in mvwprintz( w, point( column, ++vStart ), c_yellow, _( "Aware of your presence!" ) ); } + if( !inv.empty() ) { + mvwprintz( w, point( column, ++vStart ), c_white, _( "It is carrying something." ) ); + } + std::string effects = get_effect_status(); if( !effects.empty() ) { trim_and_print( w, point( column, ++vStart ), getmaxx( w ) - 2, h_white, effects ); @@ -727,7 +734,8 @@ std::string monster::extended_description() const {swims(), pgettext( "Swim as an action", "swim" )}, {flies(), pgettext( "Fly as an action", "fly" )}, {can_dig(), pgettext( "Dig as an action", "dig" )}, - {climbs(), pgettext( "Climb as an action", "climb" )} + {climbs(), pgettext( "Climb as an action", "climb" )}, + {m_flag::MF_EATS_FOOD, pgettext( "Eats as an action", "eat" )} } ); describe_flags( _( "In fight it can %s." ), { @@ -973,6 +981,7 @@ Creature::Attitude monster::attitude_to( const Creature &other ) const case MATT_FPASSIVE: case MATT_FLEE: case MATT_IGNORE: + case MATT_LOOT: case MATT_FOLLOW: return A_NEUTRAL; case MATT_ATTACK: @@ -1096,7 +1105,11 @@ monster_attitude monster::attitude( const Character *u ) const if( get_hp() != get_hp_max() ) { return MATT_FLEE; } else { - return MATT_IGNORE; + if( has_effect( effect_looting ) ) { + return MATT_LOOT; + } else { + return MATT_IGNORE; + } } } @@ -2289,6 +2302,7 @@ void monster::drop_items_on_death() if( is_hallucination() ) { return; } + if( type->death_drops.empty() ) { return; } diff --git a/src/monster.h b/src/monster.h index 1493e5c964ef6..404dbfa880a04 100644 --- a/src/monster.h +++ b/src/monster.h @@ -28,6 +28,7 @@ #include "units.h" #include "point.h" #include "value_ptr.h" +#include "item_location.h" class JsonObject; class JsonIn; @@ -38,7 +39,6 @@ class effect; struct dealt_projectile_attack; struct pathfinding_settings; struct trap; - enum class mon_trigger; class monster; @@ -62,6 +62,7 @@ enum monster_attitude { MATT_FOLLOW, MATT_ATTACK, MATT_ZLAVE, + MATT_LOOT, NUM_MONSTER_ATTITUDES }; @@ -211,6 +212,16 @@ class monster : public Creature // How good of a target is given creature (checks for visibility) float rate_target( Creature &c, float best, bool smart = false ) const; + + // How good the food is for given creature to loot. + bool eat_from_inventory( int amount = 1 ); + template + static bool is_item_lootable( const item &itm, const L &list, const P &predicate, + bool requires_all ); + item_location select_desired_loot( std::map &loot ); + std::map find_loot_in_radius( const tripoint &target, int radius = 12 ); + + void plan(); void move(); // Actual movement void footsteps( const tripoint &p ); // noise made by movement @@ -261,6 +272,17 @@ class monster : public Creature */ bool attack_at( const tripoint &p ); + /** + * Pickup item at the given location. + * + * Will only pickup item if target location matches item location + * + * @return true if something was picked up, false otherwise + */ + bool pickup_at( const tripoint &p, item_location &target ); + + + /** * Try to smash/bash/destroy your way through the terrain at p. * @@ -545,6 +567,7 @@ class monster : public Creature std::map special_attacks; tripoint goal; tripoint position; + item_location item_goal; bool dead; /** Legacy loading logic for monsters that are packing ammo. **/ void normalize_ammo( int old_ammo ); @@ -563,6 +586,9 @@ class monster : public Creature std::bitset effect_cache; cata::optional summon_time_limit = cata::nullopt; + /** Variables for monsters that steal **/ + mlootable lootable; + player *find_dragged_foe(); void nursebot_operate( player *dragged_foe ); diff --git a/src/monstergenerator.cpp b/src/monstergenerator.cpp index f11ba0e9c827a..f6ebff37f3089 100644 --- a/src/monstergenerator.cpp +++ b/src/monstergenerator.cpp @@ -170,6 +170,7 @@ std::string enum_to_string( m_flag data ) case MF_STUN_IMMUNE: return "STUN_IMMUNE"; case MF_LOUDMOVES: return "LOUDMOVES"; case MF_DROPS_AMMO: return "DROPS_AMMO"; + case MF_EATS_FOOD: return "EATS_FOOD"; // *INDENT-ON* case m_flag::MF_MAX: break; @@ -844,6 +845,38 @@ void mtype::load( const JsonObject &jo, const std::string &src ) reproduces = true; } + + //Items that are looted by the monster. + if( jo.has_member( "loots" ) ) { + JsonObject lootables = jo.get_object( "loots" ); + + for( const std::string line : lootables.get_array( "categories" ) ) { + loot.categories.push_back( line ); + } + + for( const std::string line : lootables.get_array( "materials" ) ) { + loot.materials.push_back( material_id( line ) ); + } + + for( const std::string line : lootables.get_array( "comestible_type" ) ) { + loot.comestibles.push_back( line ); + } + + for( const std::string line : lootables.get_array( "item_group" ) ) { + loot.itemsgroups.push_back( line ); + } + + if( jo.has_bool( "requires_all" ) ) { + loot.requires_all = lootables.get_bool( "requires_all" ); + } + + if( jo.has_bool( "paths_to" ) ) { + loot.paths_to = lootables.get_bool( "paths_to" ); + } + + loot.loots = true; + } + if( jo.has_member( "baby_flags" ) ) { // Because this determines mating season and some monsters have a mating season but not in-game offspring, declare this separately baby_flags.clear(); diff --git a/src/mtype.h b/src/mtype.h index ddd1b12d09bdf..a356b7508120f 100644 --- a/src/mtype.h +++ b/src/mtype.h @@ -20,6 +20,7 @@ class Creature; class monster; + template struct enum_traits; struct dealt_projectile_attack; struct species_type; @@ -174,6 +175,7 @@ enum m_flag : int { MF_CAN_OPEN_DOORS, // This monster can open doors. MF_STUN_IMMUNE, // This monster is immune to the stun effect MF_DROPS_AMMO, // This monster drops ammo. Check to make sure starting_ammo paramter is present for this monster type! + MF_EATS_FOOD, // This monster eats food. MF_MAX // Sets the length of the flags - obviously must be LAST }; @@ -197,6 +199,16 @@ struct mon_effect_data { chance( nchance ) {} }; +struct mlootable { + std::vector categories; + std::vector materials; + std::vector comestibles; + std::vector itemsgroups; + bool requires_all = false; + bool paths_to = false; + bool loots = false; +}; + struct mtype { private: friend class MonsterGenerator; @@ -300,6 +312,10 @@ struct mtype { // Note that this can be anything, and is not necessarily beneficial to the monster mon_action_defend sp_defense; + // Monster stealable variables + mlootable loot; + + // Monster upgrade variables int half_life; int age_grow; diff --git a/src/npc.cpp b/src/npc.cpp index 49b3333098f0c..7283f77ccee80 100644 --- a/src/npc.cpp +++ b/src/npc.cpp @@ -2026,6 +2026,7 @@ Creature::Attitude npc::attitude_to( const Creature &other ) const case MATT_FOLLOW: case MATT_FPASSIVE: case MATT_IGNORE: + case MATT_LOOT: case MATT_FLEE: return A_NEUTRAL; case MATT_FRIEND: diff --git a/src/player.cpp b/src/player.cpp index 0eca3c2d5876d..4bd7b74253de9 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -5165,6 +5165,7 @@ Creature::Attitude player::attitude_to( const Creature &other ) const case MATT_FOLLOW: case MATT_FPASSIVE: case MATT_IGNORE: + case MATT_LOOT: case MATT_FLEE: return A_NEUTRAL; // player does not want to harm those. diff --git a/src/savegame_json.cpp b/src/savegame_json.cpp index 00e2ca13f295a..bfaf219d1772a 100644 --- a/src/savegame_json.cpp +++ b/src/savegame_json.cpp @@ -1947,6 +1947,8 @@ void monster::load( const JsonObject &data ) data.read( "destination", destination ); goal = pos() + destination; + data.read( "item_goal", item_goal ); + upgrades = data.get_bool( "upgrades", type->upgrades ); upgrade_time = data.get_int( "upgrade_time", -1 ); @@ -2043,6 +2045,7 @@ void monster::store( JsonOut &json ) const } // Store the relative position of the goal so it loads correctly after a map shift. json.member( "destination", goal - pos() ); + json.member( "item_goal", item_goal ); json.member( "ammo", ammo ); json.member( "underwater", underwater ); json.member( "upgrades", upgrades );