Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Monster item aware behaviour #38303

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions data/json/effects.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions doc/JSON_FLAGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
42 changes: 42 additions & 0 deletions doc/MONSTERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
1 change: 1 addition & 0 deletions src/cata_string_consts.h
Original file line number Diff line number Diff line change
Expand Up @@ -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" );
Expand Down
214 changes: 214 additions & 0 deletions src/monmove.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down Expand Up @@ -277,6 +287,123 @@ float monster::rate_target( Creature &c, float best, bool smart ) const
return FLT_MAX;
}

template<typename L, typename P>
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<item *, const tripoint> monster::find_loot_in_radius( const tripoint &target, int radius )
{
std::map<item *, const tripoint> 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<item *, const tripoint> &loot )
{
std::map<item *, const tripoint>::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();
Expand Down Expand Up @@ -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<item *, const tripoint> 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() );
Expand Down Expand Up @@ -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).
Expand All @@ -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 ) );

Expand Down Expand Up @@ -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();
Diabolus-Engi marked this conversation as resolved.
Show resolved Hide resolved
}

//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 );
Night-Pryanik marked this conversation as resolved.
Show resolved Hide resolved
}

mod_moves( -100 );
return true;
}
Diabolus-Engi marked this conversation as resolved.
Show resolved Hide resolved

//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 ) ) {
Expand Down
Loading