Merge branch 'zesterer/small-fixes' into 'master'

NPC AI bug fixes, improvements

See merge request veloren/veloren!767
This commit is contained in:
Joshua Barretto 2020-01-27 19:05:07 +00:00
commit 3bd3422f8a
5 changed files with 90 additions and 41 deletions

View File

@ -8,13 +8,25 @@ pub enum Alignment {
Wild, Wild,
Enemy, Enemy,
Npc, Npc,
Owned(EcsEntity),
} }
impl Alignment { impl Alignment {
// Always attacks
pub fn hostile_towards(self, other: Alignment) -> bool { pub fn hostile_towards(self, other: Alignment) -> bool {
match (self, other) { match (self, other) {
(Alignment::Wild, Alignment::Npc) => false, (Alignment::Enemy, Alignment::Enemy) => false,
_ => self != other, (Alignment::Enemy, _) => true,
(_, Alignment::Enemy) => true,
_ => false,
}
}
// Never attacks
pub fn passive_towards(self, other: Alignment) -> bool {
match (self, other) {
(Alignment::Owned(a), Alignment::Owned(b)) if a == b => true,
_ => false,
} }
} }
} }
@ -25,17 +37,11 @@ impl Component for Alignment {
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct Agent { pub struct Agent {
pub owner: Option<EcsEntity>,
pub patrol_origin: Option<Vec3<f32>>, pub patrol_origin: Option<Vec3<f32>>,
pub activity: Activity, pub activity: Activity,
} }
impl Agent { impl Agent {
pub fn with_pet(mut self, owner: EcsEntity) -> Self {
self.owner = Some(owner);
self
}
pub fn with_patrol_origin(mut self, origin: Vec3<f32>) -> Self { pub fn with_patrol_origin(mut self, origin: Vec3<f32>) -> Self {
self.patrol_origin = Some(origin); self.patrol_origin = Some(origin);
self self
@ -50,7 +56,12 @@ impl Component for Agent {
pub enum Activity { pub enum Activity {
Idle(Vec2<f32>), Idle(Vec2<f32>),
Follow(EcsEntity, Chaser), Follow(EcsEntity, Chaser),
Attack(EcsEntity, Chaser, f64), Attack {
target: EcsEntity,
chaser: Chaser,
time: f64,
been_close: bool,
},
} }
impl Activity { impl Activity {
@ -63,7 +74,7 @@ impl Activity {
pub fn is_attack(&self) -> bool { pub fn is_attack(&self) -> bool {
match self { match self {
Activity::Attack(_, _, _) => true, Activity::Attack { .. } => true,
_ => false, _ => false,
} }
} }

View File

@ -219,11 +219,15 @@ where
is_walkable(&(pos + Vec3::new(0, -1, 0))), is_walkable(&(pos + Vec3::new(0, -1, 0))),
]; ];
const DIAGONALS: [(Vec3<i32>, [usize; 2]); 4] = [ const DIAGONALS: [(Vec3<i32>, [usize; 2]); 8] = [
(Vec3::new(1, 1, 0), [0, 2]), (Vec3::new(1, 1, 0), [0, 2]),
(Vec3::new(-1, 1, 0), [1, 2]), (Vec3::new(-1, 1, 0), [1, 2]),
(Vec3::new(1, -1, 0), [0, 3]), (Vec3::new(1, -1, 0), [0, 3]),
(Vec3::new(-1, -1, 0), [1, 3]), (Vec3::new(-1, -1, 0), [1, 3]),
(Vec3::new(1, 1, 1), [0, 2]),
(Vec3::new(-1, 1, 1), [1, 2]),
(Vec3::new(1, -1, 1), [0, 3]),
(Vec3::new(-1, -1, 1), [1, 3]),
]; ];
DIRS.iter() DIRS.iter()

View File

@ -6,7 +6,7 @@ use crate::{
sync::UidAllocator, sync::UidAllocator,
vol::ReadVol, vol::ReadVol,
}; };
use rand::{seq::SliceRandom, thread_rng, Rng}; use rand::{thread_rng, Rng};
use specs::{ use specs::{
saveload::{Marker, MarkerAllocator}, saveload::{Marker, MarkerAllocator},
Entities, Join, Read, ReadExpect, ReadStorage, System, WriteStorage, Entities, Join, Read, ReadExpect, ReadStorage, System, WriteStorage,
@ -75,7 +75,8 @@ impl<'a> System<'a> for Sys {
const AVG_FOLLOW_DIST: f32 = 6.0; const AVG_FOLLOW_DIST: f32 = 6.0;
const MAX_FOLLOW_DIST: f32 = 12.0; const MAX_FOLLOW_DIST: f32 = 12.0;
const MAX_CHASE_DIST: f32 = 24.0; const MAX_CHASE_DIST: f32 = 24.0;
const SIGHT_DIST: f32 = 30.0; const SEARCH_DIST: f32 = 30.0;
const SIGHT_DIST: f32 = 64.0;
const MIN_ATTACK_DIST: f32 = 3.25; const MIN_ATTACK_DIST: f32 = 3.25;
let mut do_idle = false; let mut do_idle = false;
@ -146,18 +147,24 @@ impl<'a> System<'a> for Sys {
do_idle = true; do_idle = true;
} }
} }
Activity::Attack(target, chaser, _) => { Activity::Attack {
target,
chaser,
been_close,
..
} => {
if let (Some(tgt_pos), _tgt_stats, tgt_alignment) = ( if let (Some(tgt_pos), _tgt_stats, tgt_alignment) = (
positions.get(*target), positions.get(*target),
stats.get(*target), stats.get(*target),
alignments.get(*target), alignments
.get(*target)
.copied()
.unwrap_or(Alignment::Owned(*target)),
) { ) {
// Don't attack aligned entities // Don't attack entities we are passive towards
// TODO: This is a bit of a hack, find a better way to do this // TODO: This is here, it's a bit of a hack
if let (Some(alignment), Some(tgt_alignment)) = if let Some(alignment) = alignment {
(alignment, tgt_alignment) if (*alignment).passive_towards(tgt_alignment) {
{
if !tgt_alignment.hostile_towards(*alignment) {
do_idle = true; do_idle = true;
break 'activity; break 'activity;
} }
@ -172,7 +179,13 @@ impl<'a> System<'a> for Sys {
.unwrap_or(Vec2::unit_y()) .unwrap_or(Vec2::unit_y())
* 0.01; * 0.01;
inputs.primary.set_state(true); inputs.primary.set_state(true);
} else if dist_sqrd < MAX_CHASE_DIST.powf(2.0) { } else if dist_sqrd < MAX_CHASE_DIST.powf(2.0)
|| (dist_sqrd < SIGHT_DIST.powf(2.0) && !*been_close)
{
if dist_sqrd < MAX_CHASE_DIST.powf(2.0) {
*been_close = true;
}
// Long-range chase // Long-range chase
if let Some(bearing) = if let Some(bearing) =
chaser.chase(&*terrain, pos.0, tgt_pos.0, 1.25) chaser.chase(&*terrain, pos.0, tgt_pos.0, 1.25)
@ -182,6 +195,12 @@ impl<'a> System<'a> for Sys {
.unwrap_or(Vec2::zero()); .unwrap_or(Vec2::zero());
inputs.jump.set_state(bearing.z > 1.0); inputs.jump.set_state(bearing.z > 1.0);
} }
if dist_sqrd < (MAX_CHASE_DIST * 0.65).powf(2.0)
&& thread_rng().gen::<f32>() < 0.01
{
inputs.roll.set_state(true);
}
} else { } else {
do_idle = true; do_idle = true;
} }
@ -201,21 +220,26 @@ impl<'a> System<'a> for Sys {
if choose_target { if choose_target {
// Search for new targets (this looks expensive, but it's only run occasionally) // Search for new targets (this looks expensive, but it's only run occasionally)
// TODO: Replace this with a better system that doesn't consider *all* entities // TODO: Replace this with a better system that doesn't consider *all* entities
let entities = (&entities, &positions, &stats, alignments.maybe()) let closest_entity = (&entities, &positions, &stats, alignments.maybe())
.join() .join()
.filter(|(e, e_pos, e_stats, e_alignment)| { .filter(|(e, e_pos, e_stats, e_alignment)| {
(e_pos.0 - pos.0).magnitude_squared() < SIGHT_DIST.powf(2.0) e_pos.0.distance_squared(pos.0) < SEARCH_DIST.powf(2.0)
&& *e != entity && *e != entity
&& !e_stats.is_dead && !e_stats.is_dead
&& alignment && alignment
.and_then(|a| e_alignment.map(|b| a.hostile_towards(*b))) .and_then(|a| e_alignment.map(|b| a.hostile_towards(*b)))
.unwrap_or(false) .unwrap_or(false)
}) })
.map(|(e, _, _, _)| e) .min_by_key(|(_, e_pos, _, _)| (e_pos.0.distance_squared(pos.0) * 100.0) as i32)
.collect::<Vec<_>>(); .map(|(e, _, _, _)| e);
if let Some(target) = (&entities).choose(&mut thread_rng()).cloned() { if let Some(target) = closest_entity {
agent.activity = Activity::Attack(target, Chaser::default(), time.0); agent.activity = Activity::Attack {
target,
chaser: Chaser::default(),
time: time.0,
been_close: false,
};
} }
} }
@ -229,8 +253,12 @@ impl<'a> System<'a> for Sys {
if !agent.activity.is_attack() { if !agent.activity.is_attack() {
if let Some(attacker) = uid_allocator.retrieve_entity_internal(by.id()) if let Some(attacker) = uid_allocator.retrieve_entity_internal(by.id())
{ {
agent.activity = agent.activity = Activity::Attack {
Activity::Attack(attacker, Chaser::default(), time.0); target: attacker,
chaser: Chaser::default(),
time: time.0,
been_close: false,
};
} }
} }
} }
@ -238,7 +266,7 @@ impl<'a> System<'a> for Sys {
} }
// Follow owner if we're too far, or if they're under attack // Follow owner if we're too far, or if they're under attack
if let Some(owner) = agent.owner { if let Some(Alignment::Owned(owner)) = alignment.copied() {
if let Some(owner_pos) = positions.get(owner) { if let Some(owner_pos) = positions.get(owner) {
let dist_sqrd = pos.0.distance_squared(owner_pos.0); let dist_sqrd = pos.0.distance_squared(owner_pos.0);
if dist_sqrd > MAX_FOLLOW_DIST.powf(2.0) && !agent.activity.is_follow() { if dist_sqrd > MAX_FOLLOW_DIST.powf(2.0) && !agent.activity.is_follow() {
@ -255,8 +283,12 @@ impl<'a> System<'a> for Sys {
if let Some(attacker) = if let Some(attacker) =
uid_allocator.retrieve_entity_internal(by.id()) uid_allocator.retrieve_entity_internal(by.id())
{ {
agent.activity = agent.activity = Activity::Attack {
Activity::Attack(attacker, Chaser::default(), time.0); target: attacker,
chaser: Chaser::default(),
time: time.0,
been_close: false,
};
} }
} }
} }

View File

@ -458,7 +458,7 @@ fn handle_tp(server: &mut Server, entity: EcsEntity, args: String, action: &Chat
fn handle_spawn(server: &mut Server, entity: EcsEntity, args: String, action: &ChatCommand) { fn handle_spawn(server: &mut Server, entity: EcsEntity, args: String, action: &ChatCommand) {
match scan_fmt_some!(&args, action.arg_fmt, String, NpcKind, String) { match scan_fmt_some!(&args, action.arg_fmt, String, NpcKind, String) {
(Some(opt_align), Some(id), opt_amount) => { (Some(opt_align), Some(id), opt_amount) => {
if let Some(alignment) = parse_alignment(&opt_align) { if let Some(alignment) = parse_alignment(entity, &opt_align) {
let amount = opt_amount let amount = opt_amount
.and_then(|a| a.parse().ok()) .and_then(|a| a.parse().ok())
.filter(|x| *x > 0) .filter(|x| *x > 0)
@ -467,11 +467,12 @@ fn handle_spawn(server: &mut Server, entity: EcsEntity, args: String, action: &C
match server.state.read_component_cloned::<comp::Pos>(entity) { match server.state.read_component_cloned::<comp::Pos>(entity) {
Some(pos) => { Some(pos) => {
let agent = if let comp::Alignment::Npc = alignment { let agent =
comp::Agent::default().with_pet(entity) if let comp::Alignment::Owned(_) | comp::Alignment::Npc = alignment {
} else { comp::Agent::default()
comp::Agent::default().with_patrol_origin(pos.0) } else {
}; comp::Agent::default().with_patrol_origin(pos.0)
};
for _ in 0..amount { for _ in 0..amount {
let vel = Vec3::new( let vel = Vec3::new(
@ -583,11 +584,12 @@ fn handle_help(server: &mut Server, entity: EcsEntity, _args: String, _action: &
} }
} }
fn parse_alignment(alignment: &str) -> Option<comp::Alignment> { fn parse_alignment(owner: EcsEntity, alignment: &str) -> Option<comp::Alignment> {
match alignment { match alignment {
"wild" => Some(comp::Alignment::Wild), "wild" => Some(comp::Alignment::Wild),
"enemy" => Some(comp::Alignment::Enemy), "enemy" => Some(comp::Alignment::Enemy),
"npc" => Some(comp::Alignment::Npc), "npc" => Some(comp::Alignment::Npc),
"pet" => Some(comp::Alignment::Owned(owner)),
_ => None, _ => None,
} }
} }

View File

@ -300,7 +300,7 @@ impl Server {
state.write_component(entity, comp::Ori(Vec3::unit_y())); state.write_component(entity, comp::Ori(Vec3::unit_y()));
state.write_component(entity, comp::Gravity(1.0)); state.write_component(entity, comp::Gravity(1.0));
state.write_component(entity, comp::CharacterState::default()); state.write_component(entity, comp::CharacterState::default());
state.write_component(entity, comp::Alignment::Npc); state.write_component(entity, comp::Alignment::Owned(entity));
state.write_component(entity, comp::Inventory::default()); state.write_component(entity, comp::Inventory::default());
state.write_component(entity, comp::InventoryUpdate); state.write_component(entity, comp::InventoryUpdate);
// Make sure physics are accepted. // Make sure physics are accepted.