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 10 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
2 changes: 1 addition & 1 deletion data/json/monsters/mammal.json
Original file line number Diff line number Diff line change
Expand Up @@ -1764,7 +1764,7 @@
"vision_night": 5,
"anger_triggers": [ "FRIEND_ATTACKED", "HURT" ],
"death_function": [ "NORMAL" ],
"flags": [ "SEES", "HEARS", "SMELLS", "ANIMAL", "PATH_AVOID_DANGER_1", "WARM", "KEENNOSE", "BLEED" ]
"flags": [ "SEES", "HEARS", "SMELLS", "ANIMAL", "PATH_AVOID_DANGER_1", "WARM", "KEENNOSE", "BLEED", "STEALS_FOOD" ]
},
{
"id": "mon_rat_king",
Expand Down
2 changes: 2 additions & 0 deletions doc/JSON_FLAGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -964,6 +964,8 @@ Multiple death functions can be used. Not all combinations make sense.
- ```SLUDGEPROOF``` Ignores the effect of sludge trails.
- ```SLUDGETRAIL``` Causes the monster to leave a sludge trap trail when moving.
- ```SMELLS``` It can smell you.
- ```STEALS_FOOD``` Will steal and eat food from the ground.
- ```STEALS_SHINY``` Will steal items shiny items with materials like gold, silver, diamond and platinum.
- ```STUMBLES``` Stumbles in its movement.
- ```SUNDEATH``` Dies in full sunlight.
- ```SWARMS``` Groups together and form loose packs.
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 @@ -307,6 +307,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_masked_scent( "masked_scent" );
static const efftype_id effect_melatonin_supplements( "melatonin" );
Expand Down
213 changes: 213 additions & 0 deletions src/monmove.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,13 @@
#include "string_formatter.h"
#include "cata_string_consts.h"

//Related to monster loot
#include "item.h"
#include "itype.h"
#include "item_location.h"

#define MONSTER_FOLLOW_DIST 8
#define MONSTER_LOOT_DIST 16

bool monster::wander()
{
Expand Down Expand Up @@ -271,6 +277,113 @@ float monster::rate_target( Creature &c, float best, bool smart ) const
return INT_MAX;
}

std::map<item *, const tripoint> monster::find_loot_in_radius( const tripoint &target, int radius )
{
const bool steals_food = has_flag( MF_STEALS_FOOD );
const bool steals_shiny = has_flag( MF_STEALS_SHINY );
bool suitable = false;
Diabolus-Engi marked this conversation as resolved.
Show resolved Hide resolved
std::map<item *, const tripoint> lootable;

//Check all tiles nearby within radius
for( const tripoint &p : g->m.points_in_radius( target, radius ) ) {
//Check if we can see some items on the tile
Diabolus-Engi marked this conversation as resolved.
Show resolved Hide resolved
if( g->m.sees_some_items( p, *this ) ) {
//A candidate tile with some items!
auto items = g->m.i_at( p );
//Check all items on tile
for( item &itm : items ) {
suitable = false;

//Does monster try to steal food and is it actually food?
if( steals_food && itm.is_comestible() && itm.get_comestible()->comesttype == "FOOD" ) {
if( !itm.rotten() ) {
Diabolus-Engi marked this conversation as resolved.
Show resolved Hide resolved
suitable = true;
}

//TODO: Sanity checks here on what we're trying to loot.
}

//Does monster steal shiny items?
if( steals_shiny &&
( itm.made_of( material_id( "gold" ) ) ||
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably better to JSONize the list of stealable materials, with monster's entry having an array of materials that it'd consider stealing (if any), listing both food and non-food materials. This would also be useful, for instance, to make carnivore food stealers steal only meat-based foods they'd actually want to eat; monsters that actually eat items they stole (if they're edible at all) would have a flag noting this, so you don't have to make two mostly-separate checks for food stealing and "shinies" stealing.

itm.made_of( material_id( "silver" ) ) ||
itm.made_of( material_id( "diamond" ) ) ||
itm.made_of( material_id( "platinum" ) ) ) ) {

//TODO: Sanity checks on what we're trying to loot here

suitable = true;
}


if( suitable ) {
lootable.insert( std::make_pair( &itm, p ) );
}
}
}
}

return lootable;
}

bool monster::eat_from_inventory()
{
auto it = inv.begin();
for( item &itm : inv ) {
//Is food?
if( itm.is_comestible() && itm.get_comestible()->comesttype == "FOOD" ) {
if( itm.charges > 1 ) {
itm.mod_charges( -1 );
Diabolus-Engi marked this conversation as resolved.
Show resolved Hide resolved
}

if( g->u.sees( *this ) ) {
add_msg( m_warning, "%1$s eats some of %2$s!", name(), itm.display_name() );
}

//Remove item
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 = MONSTER_LOOT_DIST + 1;

item *i;
tripoint p;
int d;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All these variables are used only in the cycle below, so they should be placed inside it.


//Iterate through the loot, find closest most viable item to loot
for( it = loot.begin(); it != loot.end(); it++ ) {
i = it->first;
p = it->second;
d = rl_dist_fast( pos(), p );

//Closest is preferable here...
if( d < desired_range ) {
//Ensure that can also see the target...
Diabolus-Engi marked this conversation as resolved.
Show resolved Hide resolved
if( sees( p ) ) {
desired_i = i;
desired_p = p;
desired_range = d;
}
}
}

return item_location( map_cursor( desired_p ), desired_i );
}

void monster::plan()
{
const auto &factions = g->critter_tracker->factions();
Expand All @@ -291,9 +404,15 @@ void monster::plan()
const int fears_hostile_near = type->has_fear_trigger( mon_trigger::HOSTILE_CLOSE ) ? 5 : 0;

bool group_morale = has_flag( MF_GROUP_MORALE ) && morale < type->morale;
const bool steals_food = has_flag( MF_STEALS_FOOD );
const bool will_steal = has_flag( MF_STEALS_FOOD ) || has_flag( MF_STEALS_SHINY );

bool swarms = has_flag( MF_SWARMS );

auto mood = attitude();



// If we can see the player, move toward them or flee, simpleminded animals are too dumb to follow the player.
if( friendly == 0 && sees( g->u ) && !has_flag( MF_PET_WONT_FOLLOW ) ) {
dist = rate_target( g->u, dist, smart_planning );
Expand Down Expand Up @@ -344,6 +463,31 @@ 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( will_steal && inv.empty() && one_in( 100 ) ) {
//Find all lootable items within radius
std::map<item *, const tripoint> lootable_items = find_loot_in_radius( pos(), MONSTER_LOOT_DIST );

if( !lootable_items.empty() ) {
item_goal = select_desired_loot( lootable_items );


set_dest( item_goal.position() );
add_effect( effect_looting, 50_turns );
}
} //Monster has something in inventory, will steal and with some luck...
else if( will_steal && !inv.empty() && one_in( 500 ) ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably should consider that most monsters that could be interested in food (wolves?) would just start eating food right after moving close to it, rather than picking it up and eating some time later.

Also I'm not sure how food stealing and eating would work with food in sealed tin cans (or other sealed containers); an animal very likely wouldn't be interested at all in such an item (it doesn't smell like food) and certainly shouldn't be able to eat the food inside unless it can chew through the container.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was going to add the option for animals to eat the item rather than pick it up, but felt may be out of scope. I’ll add this and the checking of containers

//If a food stealer, then eat something.
if( steals_food ) {
if( !eat_from_inventory() ) {
//TODO: Do we need to do something if fails?
}
}
}
}

if( docile ) {
if( friendly != 0 && target != nullptr ) {
set_dest( target->pos() );
Expand Down Expand Up @@ -903,6 +1047,7 @@ void monster::move()
}
}
const bool can_open_doors = has_flag( MF_CAN_OPEN_DOORS );
const bool will_steal = has_flag( MF_STEALS_FOOD ) || has_flag( MF_STEALS_SHINY );
// Finished logic section. By this point, we should have chosen a square to
// move to (moved = true).
const tripoint local_next_step = g->m.getlocal( next_step );
Expand All @@ -911,6 +1056,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 && will_steal && pickup_at( local_next_step, item_goal ) ) ||
( !pacified && push_to( local_next_step, 0, 0 ) ) ||
move_to( local_next_step, false, get_stagger_adjust( pos(), destination, local_next_step ) );

Expand Down Expand Up @@ -1381,6 +1527,73 @@ bool monster::attack_at( const tripoint &p )
return false;
}

bool monster::pickup_at( const tripoint &p, item_location &target )
{
//If called through normal pathfinding, then clear looting effect
if( p == goal ) {
remove_effect( effect_looting );

}

//Item has moved...
if( p != target.position() ) {
return false;
}

inv.push_back( *target );
item *stored_item = &inv.back();
Diabolus-Engi marked this conversation as resolved.
Show resolved Hide resolved

//Get weight of the stack or item
units::mass weight = target->weight();
units::mass capacity = this->weight_capacity();

int amount_taken = 1;

int charges = target->charges;

if( charges >= 1 ) {
Diabolus-Engi marked this conversation as resolved.
Show resolved Hide resolved
//Adjust volume and weight for units in a stack
units::mass weigh_each = weight / charges;
amount_taken = charges + ( weigh_each / capacity );
Copy link
Contributor

@Night-Pryanik Night-Pryanik Feb 24, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make sure capacity doesn't equal to zero, otherwise we'll get a division by zero here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure about this calculation? / capacity means the stronger the monster is (= higher weight capacity), so less charges it will pick up.

Also: why are you adding this value to the existing charges of the item. The result can only be larger than the number of existing charges, so the code will try to pick up more than there is.

amount_taken = std::min( target->charges, capacity / weight_each );

The std::min is so it selects the smaller value of the amount of existing charges or the amount of charges that can be picked.


//Caps the amount taken to avoid taking large stacks.
amount_taken = std::min( amount_taken, 2 );
if( amount_taken > 2 ) {
amount_taken = 2;
}

target->mod_charges( -amount_taken );
inv.back().charges = amount_taken;

//If we've taken all the stack, let's remove the item
if( amount_taken == charges ) {
target.remove_item();
}
} else {
//Remove item from ground
target.remove_item();
Diabolus-Engi marked this conversation as resolved.
Show resolved Hide resolved
}

//Successfully taken any?
if( amount_taken >= 1 ) {
//Notify the player
if( g->u.sees( *this ) ) {
add_msg( m_warning, "%1$s grabs %2$s!", name(), stored_item->display_name() );
Diabolus-Engi marked this conversation as resolved.
Show resolved Hide resolved
}


mod_moves( -100 );
return true;
} else {
//Failed to take any items, so remove the last item added to inventory.
inv.pop_back();
}
Diabolus-Engi marked this conversation as resolved.
Show resolved Hide resolved


add_msg( m_warning, "%1$s fails to grab %2$s!", name(), target->display_name() );
Diabolus-Engi marked this conversation as resolved.
Show resolved Hide resolved
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
34 changes: 30 additions & 4 deletions src/monster.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ static const std::map<monster_attitude, std::pair<std::string, color_id>> 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}},
};

Expand Down Expand Up @@ -539,6 +540,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!" ) );
Diabolus-Engi marked this conversation as resolved.
Show resolved Hide resolved
}

std::string effects = get_effect_status();
if( !effects.empty() ) {
trim_and_print( w, point( column, ++vStart ), getmaxx( w ) - 2, h_white, effects );
Expand Down Expand Up @@ -655,7 +660,9 @@ 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_STEALS_FOOD, pgettext( "Steal food as an action", "steal" )},
{m_flag::MF_STEALS_SHINY, pgettext( "Steal shiny items as an action", "steal" )}
} );

describe_flags( _( "<bad>In fight it can %s.</bad>" ), {
Expand Down Expand Up @@ -901,6 +908,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:
Expand Down Expand Up @@ -1024,7 +1032,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;
}
}
}

Expand Down Expand Up @@ -2213,11 +2225,16 @@ void monster::drop_items_on_death()
if( is_hallucination() ) {
return;
}
if( type->death_drops.empty() ) {

if( type->death_drops.empty() && inv.empty() ) {
return;
}

std::vector<item> items = item_group::items_from( type->death_drops, calendar::start_of_cataclysm );
std::vector<item> items;

if( !type->death_drops.empty() ) {
items = item_group::items_from( type->death_drops, calendar::start_of_cataclysm );
}

// This block removes some items, according to item spawn scaling factor
const float spawn_rate = get_option<float>( "ITEM_SPAWNRATE" );
Expand All @@ -2236,6 +2253,15 @@ void monster::drop_items_on_death()
items = remaining;
}

// Adds all items in the monsters inventory to be dropped.
Diabolus-Engi marked this conversation as resolved.
Show resolved Hide resolved
// TODO: Apply random damage to items ? Modify them in some way ?
items.insert(items.end(), inv.begin(), inv.end());
for( const item &itm : inv ) {
items.push_back( itm );
}

inv.clear();

const auto dropped = g->m.spawn_items( pos(), items );

if( has_flag( MF_FILTHY ) && get_option<bool>( "FILTHY_CLOTHES" ) ) {
Expand Down
Loading