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

Many small logic adjustments and bugfixes for NPC targeting / threat assessment #69840

Merged
Merged
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
2 changes: 2 additions & 0 deletions data/json/damage_types.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"magic_color": "light_red",
"name": "pierce",
"skill": "stabbing",
"mon_difficulty": true,
"//2": "derived from cut only for monster defs",
"derived_from": [ "cut", 0.8 ],
"immune_flags": { "character": [ "STAB_IMMUNE" ] }
Expand All @@ -79,6 +80,7 @@
"type": "damage_type",
"physical": true,
"magic_color": "light_red",
"mon_difficulty": true,
"name": "ballistic",
"material_required": true,
"immune_flags": { "character": [ "BULLET_IMMUNE" ] }
Expand Down
6 changes: 5 additions & 1 deletion data/json/npcs/TALK_TEST.json
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,11 @@
"topic": "TALK_DONE",
"condition": { "npc_aim_rule": "AIM_SPRAY" }
},
{ "text": "This is a npc rule test response.", "topic": "TALK_DONE", "condition": { "npc_rule": "use_silent" } }
{
"text": "This is a npc rule test response.",
"topic": "TALK_DONE",
"condition": { "npc_rule": "avoid_doors" }
}
]
},
{
Expand Down
19 changes: 12 additions & 7 deletions src/npc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ static const mfaction_str_id monfaction_bee( "bee" );
static const mfaction_str_id monfaction_human( "human" );
static const mfaction_str_id monfaction_player( "player" );

static const mon_flag_str_id mon_flag_HIT_AND_RUN( "HIT_AND_RUN" );
static const mon_flag_str_id mon_flag_RIDEABLE_MECH( "RIDEABLE_MECH" );

static const overmap_location_str_id overmap_location_source_of_ammo( "source_of_ammo" );
Expand Down Expand Up @@ -2629,7 +2630,11 @@ Creature::Attitude npc::attitude_to( const Creature &other ) const
case MATT_FPASSIVE:
case MATT_IGNORE:
case MATT_FLEE:
return Attitude::NEUTRAL;
if( m.has_flag( mon_flag_HIT_AND_RUN ) ) {
return Attitude::HOSTILE;
} else {
return Attitude::NEUTRAL;
}
case MATT_FRIEND:
return Attitude::FRIENDLY;
case MATT_ATTACK:
Expand Down Expand Up @@ -3781,22 +3786,22 @@ npc_follower_rules::npc_follower_rules()
override_enable = ally_rule::DEFAULT;

set_flag( ally_rule::use_guns );
set_flag( ally_rule::use_grenades );
clear_flag( ally_rule::use_silent );
clear_flag( ally_rule::use_grenades );
set_flag( ally_rule::use_silent );
set_flag( ally_rule::avoid_friendly_fire );

clear_flag( ally_rule::allow_pick_up );
clear_flag( ally_rule::allow_bash );
clear_flag( ally_rule::allow_sleep );
set_flag( ally_rule::allow_complain );
set_flag( ally_rule::allow_pulp );
clear_flag( ally_rule::close_doors );
clear_flag( ally_rule::follow_close );
set_flag( ally_rule::close_doors );
set_flag( ally_rule::follow_close );
clear_flag( ally_rule::avoid_doors );
clear_flag( ally_rule::hold_the_line );
clear_flag( ally_rule::ignore_noise );
set_flag( ally_rule::ignore_noise );
clear_flag( ally_rule::forbid_engage );
set_flag( ally_rule::follow_distance_2 );
clear_flag( ally_rule::follow_distance_2 );
}

bool npc_follower_rules::has_flag( ally_rule test, bool check_override ) const
Expand Down
36 changes: 20 additions & 16 deletions src/npc.h
Original file line number Diff line number Diff line change
Expand Up @@ -256,21 +256,7 @@ struct npc_opinion {
void deserialize( const JsonObject &data );
};

// npc_combat_memory should store short-term trackers that don't really need to be saved if
// the player exits the game. Minor logic behaviour changes might occur, but nothing serious.
struct npc_combat_memory {
float assess_ally = 0.0f;
float assess_enemy = 0.0f;
int panic = 0;
int swarm_count = 0; //so you can tell if you're getting away over multiple turns
int failing_to_reposition = 0; // Inc. when tries to flee/move and doesn't change assess
int reposition_countdown = 0; // set when repos fails so that we don't keep trying.
int assessment_before_repos = 0; // assessment of enemy threat level at the start of repos
float my_health = 1.0f; // saved when we evaluate_self. Health 1.0 means 100% unhurt.
bool repositioning = false; // is NPC running away or just moving around / kiting.
int formation_distance = -1; // dist to nearest ally with a gun, or to player
int engagement_distance = 6; // applies to melee NPCs in formation with ranged ones or the player.
};


enum class combat_engagement : int {
NONE = 0,
Expand Down Expand Up @@ -587,6 +573,7 @@ struct npc_short_term_cache {
npc_attack_rating current_attack_evaluation;
std::shared_ptr<npc_attack> current_attack;


// Use weak_ptr to avoid circular references between Creatures
// attitude of creatures the npc can see
std::vector<weak_ptr_fast<Creature>> hostile_guys;
Expand All @@ -602,6 +589,22 @@ struct npc_short_term_cache {
std::optional<int> closest_enemy_to_friendly_distance() const;
};

// npc_combat_memory should store short-term trackers that don't really need to be saved if
// the player exits the game. Minor logic behaviour changes might occur, but nothing serious.
struct npc_combat_memory_cache {
float assess_ally = 0.0f;
float assess_enemy = 0.0f;
int panic = 0;
int swarm_count = 0; //so you can tell if you're getting away over multiple turns
int failing_to_reposition = 0; // Inc. when tries to flee/move and doesn't change assess
int reposition_countdown = 0; // set when repos fails so that we don't keep trying.
int assessment_before_repos = 0; // assessment of enemy threat level at the start of repos
float my_health = 1.0f; // saved when we evaluate_self. Health 1.0 means 100% unhurt.
bool repositioning = false; // is NPC running away or just moving around / kiting.
int formation_distance = -1; // dist to nearest ally with a gun, or to player
int engagement_distance = 6; // applies to melee NPCs in formation with ranged ones or the player.
};

struct npc_need_goal_cache {
tripoint_abs_omt goal;
tripoint_abs_omt omt_loc;
Expand Down Expand Up @@ -1374,7 +1377,8 @@ class npc : public Character
npc_mission previous_mission = NPC_MISSION_NULL;
npc_personality personality;
npc_opinion op_of_u;
npc_combat_memory mem_combat;
npc_combat_memory_cache mem_combat;

dialogue_chatbin chatbin;
int patience = 0; // Used when we expect the player to leave the area
npc_follower_rules rules;
Expand Down
9 changes: 7 additions & 2 deletions src/npc_attack.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -273,13 +273,18 @@ void npc_attack_melee::use( npc &source, const tripoint &location ) const
if( clear_path && source.mem_combat.formation_distance == -1 ) {
source.move_to_next();
add_msg_debug( debugmode::DF_NPC_MOVEAI,
"<color_light_gray>%s has no nearby ranged allies. Going for the attack.</color>", source.name );
"<color_light_gray>%s has no nearby ranged allies. Going for attack.</color>", source.name );
} else if( clear_path && source.mem_combat.formation_distance > target_distance ) {
source.move_to_next();
add_msg_debug( debugmode::DF_NPC_MOVEAI,
"<color_light_gray>%s is at least %i away from ranged allies, enemy within %i. Going for attack.</color>",
source.name, source.mem_combat.formation_distance, target_distance );

} else if( clear_path &&
source.mem_combat.formation_distance > source.closest_enemy_to_friendly_distance() ) {
source.move_to_next();
//add_msg_debug( debugmode::DF_NPC_MOVEAI,
// "<color_light_gray>%s is at least %i away from allies, enemy within %i of ally. Going for attack.</color>",
// source.name, source.mem_combat.formation_distance, source.closest_enemy_to_friendly_distance() );
} else {
add_msg_debug( debugmode::DF_NPC_MOVEAI,
"<color_light_gray>%s can't path to melee target, or is staying close to ranged allies.</color>",
Expand Down
86 changes: 50 additions & 36 deletions src/npcmove.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -475,23 +475,16 @@ std::vector<sphere> npc::find_dangerous_explosives() const
float npc::evaluate_monster( const monster &target, int dist ) const
{
float speed = target.speed_rating();
float scaled_distance = std::max( 1.0f, dist * dist / ( speed + 10.0f ) );
float scaled_distance = std::max( 1.0f, dist * dist / ( speed * 250.0f ) );
float hp_percent = static_cast<float>( target.get_hp() ) / target.get_hp_max();
float diff = std::min( static_cast<float>( target.type->difficulty ), NPC_DANGER_VERY_LOW );
float diff = std::max( static_cast<float>( target.type->difficulty ), NPC_DANGER_VERY_LOW );
add_msg_debug( debugmode::DF_NPC_COMBATAI,
"<color_yellow>evaluate_monster </color><color_light_gray>%s thinks %s threat level is %1.2f before considering situation.</color>",
name,
target.type->nname(), diff );
"<color_yellow>evaluate_monster </color><color_dark_gray>%s thinks %s threat level is <color_light_gray>%1.2f</color><color_dark_gray> before considering situation. Speed rating: %1.2f; dist: %i; scaled_distance: %1.0f; HP: %1.0f%%</color>",
name, target.type->nname(), diff, speed, dist, scaled_distance, hp_percent * 100 );
// Note that the danger can pass below "very low" if the monster is weak and far away.
diff *= ( hp_percent * 0.5f + 0.5f ) / scaled_distance;
/*add_msg_debug( debugmode::DF_NPC_COMBATAI,
"<color_light_gray>%s distance from %s: %i. Speed rating: %1.2f. Scaled distance: %1.2f.</color>",
name, target.type->nname(), dist, speed, scaled_distance );
add_msg_debug( debugmode::DF_NPC_COMBATAI,
"<color_light_gray>%s sees %s hp percent remaining is %1.0f%%.</color>",
name, target.type->nname(), hp_percent * 100 );*/
add_msg_debug( debugmode::DF_NPC_COMBATAI,
"%s puts final %s threat level at %1.2f<color_light_gray> after counting speed, distance, hp</color>",
"<color_light_gray>%s puts final %s threat level at </color>%1.2f<color_light_gray> after counting speed, distance, hp</color>",
name, target.type->nname(), diff );
return std::min( diff, NPC_MONSTER_DANGER_MAX );
}
Expand Down Expand Up @@ -678,10 +671,11 @@ float npc::estimate_armour( const Character &candidate ) const
"<color_light_gray>%s: %s armour value for %s rated as %i.</color>", name,
candidate.disp_name( true ), body_part_name( part_id ), armour_step );
if( part_id == bodypart_id( "head" ) || part_id == bodypart_id( "torso" ) ) {
armour_step *= 3;
number_of_parts += 2;
armour_step *= 4;
number_of_parts += 3;
}
armour += static_cast<float>( armour_step );
// obtain an average value of the 4 armour types we checked.
armour += static_cast<float>( armour_step ) / 4.0f;
}
armour /= number_of_parts;

Expand All @@ -693,7 +687,6 @@ float npc::estimate_armour( const Character &candidate ) const
return armour;
}


static bool too_close( const tripoint &critter_pos, const tripoint &ally_pos, const int def_radius )
{
return rl_dist( critter_pos, ally_pos ) <= def_radius;
Expand Down Expand Up @@ -725,14 +718,20 @@ void npc::assess_danger()
int hostile_count = 0; // for tallying nearby threatening enemies
int friendly_count = 1; // count yourself as a friendly
int def_radius = rules.has_flag( ally_rule::follow_close ) ? follow_distance() : 6;
float bravery_vs_pain = static_cast<float>( personality.bravery ) - get_pain() / 10.0f;
bool npc_ranged = get_wielded_item() && get_wielded_item()->is_gun();

if( !confident_range_cache ) {
invalidate_range_cache();
}
// Radius we can attack without moving
int max_range = *confident_range_cache;
// Radius in which enemy threats are multiplied to avoid surrounding
int preferred_medium_range = std::max( max_range, 8 );
preferred_medium_range = std::min( preferred_medium_range, 15 );
// Radius in which enemy threats are hugely multiplied to encourage repositioning
int preferred_close_range = std::max( max_range, 1 );
preferred_close_range = std::min( preferred_close_range, preferred_medium_range / 2 );

Character &player_character = get_player_character();
bool sees_player = sees( player_character.pos() );
const bool self_defense_only = rules.engagement == combat_engagement::NO_MOVE ||
Expand Down Expand Up @@ -853,14 +852,14 @@ void npc::assess_danger()
if( !clear_shot_reach( pos(), critter.pos(), false ) ) {
if( is_enemy() || !critter.friendly ) {
// still warn about enemies behind impassable glass walls, but not as often.
add_msg_debug( debugmode::DF_NPC,
add_msg_debug( debugmode::DF_NPC_COMBATAI,
"%s ignored %s because there's an obstacle in between. Might warn about it.",
name, critter.type->nname() );
if( critter_threat > 2 * ( 8.0f + personality.bravery + rng( 0, 5 ) ) ) {
warn_about( "monster", 10_minutes, critter.type->nname(), dist, critter.pos() );
}
} else {
add_msg_debug( debugmode::DF_NPC,
add_msg_debug( debugmode::DF_NPC_COMBATAI,
"%s ignored %s because there's an obstacle in between, and it's not worth warning about.",
name, critter.type->nname() );
}
Expand All @@ -872,26 +871,24 @@ void npc::assess_danger()
if( critter_threat > ( 8.0f + personality.bravery + rng( 0, 5 ) ) ) {
warn_about( "monster", 10_minutes, critter.type->nname(), dist, critter.pos() );
}
if( dist < 8 && critter_threat > bravery_vs_pain ) {
if( dist < preferred_medium_range ) {
hostile_count += 1;
add_msg_debug( debugmode::DF_NPC_COMBATAI,
"<color_light_gray>%s added %s to nearby hostile count. Total: %i</color>", name,
critter.type->nname(), hostile_count );
}
if( dist < 4 && npc_ranged ) {
if( dist <= preferred_close_range ) {
mem_combat.swarm_count += 1;
add_msg_debug( debugmode::DF_NPC_COMBATAI,
"<color_light_gray>%s added %s to swarming enemies count. Total: %i</color>",
name,
critter.type->nname(), mem_combat.swarm_count );
"<color_light_gray>%s added %s to swarm count. Total: %i</color>",
name, critter.type->nname(), mem_combat.swarm_count );
}
}
if( must_retreat || no_fighting ) {
continue;
}


add_msg_debug( debugmode::DF_NPC_COMBATAI,
add_msg_debug( debugmode::DF_NPC,
"%s assessed threat of critter %s as %1.2f.",
name, critter.type->nname(), critter_threat );
ai_cache.total_danger += critter_threat;
Expand Down Expand Up @@ -1039,7 +1036,7 @@ void npc::assess_danger()
name, player_diff );
if( dist <= 3 ) {
player_diff = player_diff * ( 4 - dist ) / 2;
mem_combat.swarm_count = 0;
mem_combat.swarm_count /= ( 4 - dist );
mem_combat.assess_ally += player_diff;
add_msg_debug( debugmode::DF_NPC_COMBATAI,
"<color_green>Player is %i tiles from %s.</color><color_light_gray> Adding </color><color_light_green>%1.2f to ally strength</color><color_light_gray> and bolstering morale.</color>",
Expand Down Expand Up @@ -1152,7 +1149,6 @@ void npc::act_on_danger_assessment()
} else {
add_msg_debug( debugmode::DF_NPC_COMBATAI, "%s still wants to reposition, but they just tried.",
name );
mem_combat.reposition_countdown --;
}
mem_combat.panic *= ( mem_combat.assess_enemy / ( mem_combat.assess_ally + 0.5f ) );
mem_combat.panic += std::min(
Expand All @@ -1175,10 +1171,10 @@ void npc::act_on_danger_assessment()
}
}
}
} else if( failed_reposition || ( npc_ranged &&
mem_combat.assess_ally < mem_combat.assess_enemy * mem_combat.swarm_count ) ) {
} else if( failed_reposition ||
( mem_combat.assess_ally < mem_combat.assess_enemy * mem_combat.swarm_count ) ) {
add_msg_debug( debugmode::DF_NPC_COMBATAI,
"<color_light_gray>Due to ranged weapon, %s considers </color>repositioning<color_light_gray> from swarming enemies.</color>",
"<color_light_gray>%s considers </color>repositioning<color_light_gray> from swarming enemies.</color>",
name );
if( failed_reposition ) {
add_msg_debug( debugmode::DF_NPC_COMBATAI, "%s failed repositioning, trying again." );
Expand Down Expand Up @@ -1268,6 +1264,19 @@ void npc::regen_ai_cache()
mem_combat.assess_enemy = 0.0f;
mem_combat.assess_ally = 0.0f;
mem_combat.swarm_count = 0;
if( mem_combat.reposition_countdown > 0 ) {
mem_combat.reposition_countdown --;
}

if( mem_combat.repositioning && !has_effect( effect_npc_run_away ) &&
!has_effect( effect_npc_fire_bad ) ) {
// if NPC no longer has the run away effect and isn't fleeing in panic,
// they can stop moving away.
mem_combat.repositioning = false;
mem_combat.reposition_countdown = 1;
path.clear();
}

assess_danger();
if( old_assessment > NPC_DANGER_VERY_LOW && ai_cache.danger_assessment <= 0 ) {
warn_about( "relax", 30_minutes );
Expand Down Expand Up @@ -3707,10 +3716,15 @@ std::list<item> npc::pick_up_item_vehicle( vehicle &veh, int part_index )
bool npc::find_corpse_to_pulp()
{
Character &player_character = get_player_character();
if( ( is_player_ally() && ( !rules.has_flag( ally_rule::allow_pulp ) ||
player_character.in_vehicle ) ) ||
is_hallucination() ) {
return false;
if( is_player_ally() ) {
if( !rules.has_flag( ally_rule::allow_pulp ) ||
player_character.in_vehicle || is_hallucination() ) {
return false;
}
if( rl_dist( pos(), player_character.pos() ) >= mem_combat.engagement_distance ) {
// don't start to pulp corpses if you're already far from the player.
return false;
}
}

map &here = get_map();
Expand Down Expand Up @@ -3750,7 +3764,7 @@ bool npc::find_corpse_to_pulp()
return nullptr;
};

const int range = 6;
const int range = mem_combat.engagement_distance;

const item *corpse = nullptr;
if( pulp_location && square_dist( get_location(), *pulp_location ) <= range ) {
Expand Down
Loading
Loading