diff --git a/common/src/comp/agent.rs b/common/src/comp/agent.rs index 2585c379eb..5652a0ca87 100644 --- a/common/src/comp/agent.rs +++ b/common/src/comp/agent.rs @@ -8,13 +8,25 @@ pub enum Alignment { Wild, Enemy, Npc, + Owned(EcsEntity), } impl Alignment { + // Always attacks pub fn hostile_towards(self, other: Alignment) -> bool { match (self, other) { - (Alignment::Wild, Alignment::Npc) => false, - _ => self != other, + (Alignment::Enemy, Alignment::Enemy) => false, + (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)] pub struct Agent { - pub owner: Option, pub patrol_origin: Option>, pub activity: Activity, } 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) -> Self { self.patrol_origin = Some(origin); self @@ -50,7 +56,12 @@ impl Component for Agent { pub enum Activity { Idle(Vec2), Follow(EcsEntity, Chaser), - Attack(EcsEntity, Chaser, f64), + Attack { + target: EcsEntity, + chaser: Chaser, + time: f64, + been_close: bool, + }, } impl Activity { @@ -63,7 +74,7 @@ impl Activity { pub fn is_attack(&self) -> bool { match self { - Activity::Attack(_, _, _) => true, + Activity::Attack { .. } => true, _ => false, } } diff --git a/common/src/path.rs b/common/src/path.rs index f6244f0895..e4ea0b52c7 100644 --- a/common/src/path.rs +++ b/common/src/path.rs @@ -219,11 +219,15 @@ where is_walkable(&(pos + Vec3::new(0, -1, 0))), ]; - const DIAGONALS: [(Vec3, [usize; 2]); 4] = [ + const DIAGONALS: [(Vec3, [usize; 2]); 8] = [ (Vec3::new(1, 1, 0), [0, 2]), (Vec3::new(-1, 1, 0), [1, 2]), (Vec3::new(1, -1, 0), [0, 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() diff --git a/common/src/sys/agent.rs b/common/src/sys/agent.rs index 61f198fc94..a1506e1441 100644 --- a/common/src/sys/agent.rs +++ b/common/src/sys/agent.rs @@ -6,7 +6,7 @@ use crate::{ sync::UidAllocator, vol::ReadVol, }; -use rand::{seq::SliceRandom, thread_rng, Rng}; +use rand::{thread_rng, Rng}; use specs::{ saveload::{Marker, MarkerAllocator}, 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 MAX_FOLLOW_DIST: f32 = 12.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; let mut do_idle = false; @@ -146,18 +147,24 @@ impl<'a> System<'a> for Sys { do_idle = true; } } - Activity::Attack(target, chaser, _) => { + Activity::Attack { + target, + chaser, + been_close, + .. + } => { if let (Some(tgt_pos), _tgt_stats, tgt_alignment) = ( positions.get(*target), stats.get(*target), - alignments.get(*target), + alignments + .get(*target) + .copied() + .unwrap_or(Alignment::Owned(*target)), ) { - // Don't attack aligned entities - // TODO: This is a bit of a hack, find a better way to do this - if let (Some(alignment), Some(tgt_alignment)) = - (alignment, tgt_alignment) - { - if !tgt_alignment.hostile_towards(*alignment) { + // Don't attack entities we are passive towards + // TODO: This is here, it's a bit of a hack + if let Some(alignment) = alignment { + if (*alignment).passive_towards(tgt_alignment) { do_idle = true; break 'activity; } @@ -172,7 +179,13 @@ impl<'a> System<'a> for Sys { .unwrap_or(Vec2::unit_y()) * 0.01; 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 if let Some(bearing) = chaser.chase(&*terrain, pos.0, tgt_pos.0, 1.25) @@ -182,6 +195,12 @@ impl<'a> System<'a> for Sys { .unwrap_or(Vec2::zero()); inputs.jump.set_state(bearing.z > 1.0); } + + if dist_sqrd < (MAX_CHASE_DIST * 0.65).powf(2.0) + && thread_rng().gen::() < 0.01 + { + inputs.roll.set_state(true); + } } else { do_idle = true; } @@ -201,21 +220,26 @@ impl<'a> System<'a> for Sys { if choose_target { // 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 - let entities = (&entities, &positions, &stats, alignments.maybe()) + let closest_entity = (&entities, &positions, &stats, alignments.maybe()) .join() .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_stats.is_dead && alignment .and_then(|a| e_alignment.map(|b| a.hostile_towards(*b))) .unwrap_or(false) }) - .map(|(e, _, _, _)| e) - .collect::>(); + .min_by_key(|(_, e_pos, _, _)| (e_pos.0.distance_squared(pos.0) * 100.0) as i32) + .map(|(e, _, _, _)| e); - if let Some(target) = (&entities).choose(&mut thread_rng()).cloned() { - agent.activity = Activity::Attack(target, Chaser::default(), time.0); + if let Some(target) = closest_entity { + 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 let Some(attacker) = uid_allocator.retrieve_entity_internal(by.id()) { - agent.activity = - Activity::Attack(attacker, Chaser::default(), time.0); + agent.activity = Activity::Attack { + 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 - if let Some(owner) = agent.owner { + if let Some(Alignment::Owned(owner)) = alignment.copied() { if let Some(owner_pos) = positions.get(owner) { let dist_sqrd = pos.0.distance_squared(owner_pos.0); 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) = uid_allocator.retrieve_entity_internal(by.id()) { - agent.activity = - Activity::Attack(attacker, Chaser::default(), time.0); + agent.activity = Activity::Attack { + target: attacker, + chaser: Chaser::default(), + time: time.0, + been_close: false, + }; } } } diff --git a/server/src/cmd.rs b/server/src/cmd.rs index b036ae0b1b..33e0eb6ca1 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -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) { match scan_fmt_some!(&args, action.arg_fmt, String, NpcKind, String) { (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 .and_then(|a| a.parse().ok()) .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::(entity) { Some(pos) => { - let agent = if let comp::Alignment::Npc = alignment { - comp::Agent::default().with_pet(entity) - } else { - comp::Agent::default().with_patrol_origin(pos.0) - }; + let agent = + if let comp::Alignment::Owned(_) | comp::Alignment::Npc = alignment { + comp::Agent::default() + } else { + comp::Agent::default().with_patrol_origin(pos.0) + }; for _ in 0..amount { 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 { +fn parse_alignment(owner: EcsEntity, alignment: &str) -> Option { match alignment { "wild" => Some(comp::Alignment::Wild), "enemy" => Some(comp::Alignment::Enemy), "npc" => Some(comp::Alignment::Npc), + "pet" => Some(comp::Alignment::Owned(owner)), _ => None, } } diff --git a/server/src/lib.rs b/server/src/lib.rs index 2c190be2d6..880e5b3b99 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -300,7 +300,7 @@ impl Server { state.write_component(entity, comp::Ori(Vec3::unit_y())); state.write_component(entity, comp::Gravity(1.0)); 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::InventoryUpdate); // Make sure physics are accepted.