diff --git a/common/src/comp/agent.rs b/common/src/comp/agent.rs index a6a6326f6a..7f29a3d190 100644 --- a/common/src/comp/agent.rs +++ b/common/src/comp/agent.rs @@ -3,32 +3,38 @@ use specs::{Component, Entity as EcsEntity}; use specs_idvs::IDVStorage; use vek::*; -#[derive(Clone, Debug)] -pub enum Agent { - Wanderer(Vec2), - Pet { - target: EcsEntity, - chaser: Chaser, - }, - Enemy { - bearing: Vec2, - target: Option, - }, +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum Alignment { + Wild, + Enemy, + Npc, +} + +impl Alignment { + pub fn hostile_towards(self, other: Alignment) -> bool { + match (self, other) { + (Alignment::Wild, Alignment::Npc) => true, + _ => self != other, + } + } +} + +impl Component for Alignment { + type Storage = IDVStorage; +} + +#[derive(Clone, Debug, Default)] +pub struct Agent { + pub chaser: Chaser, + pub target: Option, + pub owner: Option, + pub patrol_origin: Option>, } impl Agent { - pub fn enemy() -> Self { - Agent::Enemy { - bearing: Vec2::zero(), - target: None, - } - } - - pub fn pet(target: EcsEntity) -> Self { - Agent::Pet { - target, - chaser: Chaser::default(), - } + pub fn with_pet(mut self, owner: EcsEntity) -> Self { + self.owner = Some(owner); + self } } diff --git a/common/src/comp/mod.rs b/common/src/comp/mod.rs index f084736af2..ec36ce2774 100644 --- a/common/src/comp/mod.rs +++ b/common/src/comp/mod.rs @@ -16,7 +16,7 @@ mod visual; // Reexports pub use admin::Admin; -pub use agent::Agent; +pub use agent::{Agent, Alignment}; pub use body::{ biped_large, bird_medium, bird_small, critter, dragon, fish_medium, fish_small, humanoid, object, quadruped_medium, quadruped_small, Body, diff --git a/common/src/event.rs b/common/src/event.rs index 6804577ab1..9f2d18890b 100644 --- a/common/src/event.rs +++ b/common/src/event.rs @@ -102,6 +102,7 @@ pub enum ServerEvent { stats: comp::Stats, body: comp::Body, agent: comp::Agent, + alignment: comp::Alignment, scale: comp::Scale, }, ClientDisconnect(EcsEntity), diff --git a/common/src/path.rs b/common/src/path.rs index da29fbfffc..1839d951f6 100644 --- a/common/src/path.rs +++ b/common/src/path.rs @@ -3,6 +3,7 @@ use crate::{ terrain::Block, vol::{BaseVol, ReadVol}, }; +use rand::{thread_rng, Rng}; use std::iter::FromIterator; use vek::*; @@ -89,13 +90,19 @@ pub struct Chaser { } impl Chaser { - pub fn chase(&mut self, vol: &V, pos: Vec3, tgt: Vec3) -> Option> + pub fn chase( + &mut self, + vol: &V, + pos: Vec3, + tgt: Vec3, + min_dist: f32, + ) -> Option> where V: BaseVol + ReadVol, { let pos_to_tgt = pos.distance(tgt); - if pos_to_tgt < 4.0 { + if pos_to_tgt < min_dist { return None; } @@ -104,7 +111,7 @@ impl Chaser { if end_to_tgt > pos_to_tgt * 0.3 + 5.0 { None } else { - if rand::random::() < 0.005 { + if thread_rng().gen::() < 0.005 { // TODO: Only re-calculate route when we're stuck self.route = Route::default(); } @@ -205,12 +212,7 @@ where let satisfied = |pos: &Vec3| pos == &end; let mut new_astar = match astar.take() { - None => { - let max_iters = ((Vec2::::from(startf).distance(Vec2::from(endf)) * 2.0 + 25.0) - .powf(2.0) as usize) - .min(25_000); - Astar::new(max_iters, start, heuristic.clone()) - } + None => Astar::new(20_000, start, heuristic.clone()), Some(astar) => astar, }; diff --git a/common/src/state.rs b/common/src/state.rs index 4bbaa5d3b4..1059fb20b1 100644 --- a/common/src/state.rs +++ b/common/src/state.rs @@ -141,6 +141,7 @@ impl State { ecs.register::>(); ecs.register::>(); ecs.register::(); + ecs.register::(); ecs.register::(); ecs.register::(); ecs.register::(); diff --git a/common/src/sys/agent.rs b/common/src/sys/agent.rs index ffb4e4c8a7..2be0efafae 100644 --- a/common/src/sys/agent.rs +++ b/common/src/sys/agent.rs @@ -1,20 +1,29 @@ -use crate::comp::{ - Agent, CharacterState, Controller, MountState, MovementState::Glide, Pos, Stats, -}; use crate::terrain::TerrainGrid; -use rand::{seq::SliceRandom, thread_rng}; -use specs::{Entities, Join, ReadExpect, ReadStorage, System, WriteStorage}; +use crate::{ + comp::{ + self, Agent, Alignment, CharacterState, Controller, MountState, MovementState::Glide, Pos, + Stats, + }, + sync::{Uid, UidAllocator}, +}; +use rand::{seq::SliceRandom, thread_rng, Rng}; +use specs::{ + saveload::{Marker, MarkerAllocator}, + Entities, Join, Read, ReadExpect, ReadStorage, System, WriteStorage, +}; use vek::*; /// This system will allow NPCs to modify their controller pub struct Sys; impl<'a> System<'a> for Sys { type SystemData = ( + Read<'a, UidAllocator>, Entities<'a>, ReadStorage<'a, Pos>, ReadStorage<'a, Stats>, ReadStorage<'a, CharacterState>, ReadExpect<'a, TerrainGrid>, + ReadStorage<'a, Alignment>, WriteStorage<'a, Agent>, WriteStorage<'a, Controller>, ReadStorage<'a, MountState>, @@ -23,19 +32,22 @@ impl<'a> System<'a> for Sys { fn run( &mut self, ( + uid_allocator, entities, positions, stats, character_states, terrain, + alignments, mut agents, mut controllers, mount_states, ): Self::SystemData, ) { - for (entity, pos, agent, controller, mount_state) in ( + for (entity, pos, alignment, agent, controller, mount_state) in ( &entities, &positions, + alignments.maybe(), &mut agents, &mut controllers, mount_states.maybe(), @@ -60,6 +72,99 @@ impl<'a> System<'a> for Sys { let mut inputs = &mut controller.inputs; + const PET_DIST: f32 = 12.0; + const PATROL_DIST: f32 = 48.0; + const SIGHT_DIST: f32 = 18.0; + const MIN_ATTACK_DIST: f32 = 3.25; + + let mut chase_tgt = None; + let mut choose_target = false; + + if let Some(target) = agent.target { + // Chase / attack target + if let (Some(tgt_pos), stats) = (positions.get(target), stats.get(target)) { + if stats.map(|s| s.is_dead).unwrap_or(false) { + // Don't target dead entities + choose_target = true; + } else if pos.0.distance(tgt_pos.0) < SIGHT_DIST { + chase_tgt = Some((tgt_pos.0, 1.0, true)) + } else { + // Lose sight of enemies + choose_target = true; + } + } else { + choose_target = true; + } + } else if let Some(owner) = agent.owner { + if let Some(tgt_pos) = positions.get(owner) { + if pos.0.distance(tgt_pos.0) > PET_DIST || agent.target.is_none() { + // Follow owner + chase_tgt = Some((tgt_pos.0, 6.0, false)); + } else { + choose_target = thread_rng().gen::() < 0.02; + } + } else { + agent.owner = None; + } + } else if let Some(patrol_origin) = agent.patrol_origin { + if pos.0.distance(patrol_origin) < PATROL_DIST { + // Return to patrol origin + chase_tgt = Some((patrol_origin, 64.0, false)); + } + } else { + choose_target = thread_rng().gen::() < 0.05; + } + + // Attack a target that's attacking us + if let Some(stats) = stats.get(entity) { + match stats.health.last_change.1.cause { + comp::HealthSource::Attack { by } => { + if agent.target.is_none() { + agent.target = uid_allocator.retrieve_entity_internal(by.id()); + } else if thread_rng().gen::() < 0.005 { + agent.target = uid_allocator.retrieve_entity_internal(by.id()); + } + } + _ => {} + } + } + + if choose_target { + // Search for new targets + let entities = (&entities, &positions, &stats, alignments.maybe()) + .join() + .filter(|(e, e_pos, e_stats, e_alignment)| { + (e_pos.0 - pos.0).magnitude() < SIGHT_DIST + && *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::>(); + + agent.target = (&entities).choose(&mut thread_rng()).cloned(); + } + + // Chase target + if let Some((tgt_pos, min_dist, aggressive)) = chase_tgt { + if let Some(bearing) = agent.chaser.chase(&*terrain, pos.0, tgt_pos, min_dist) { + inputs.move_dir = Vec2::from(bearing).try_normalized().unwrap_or(Vec2::zero()); + inputs.jump.set_state(bearing.z > 1.0); + } + + if pos.0.distance(tgt_pos) < MIN_ATTACK_DIST && aggressive { + inputs.look_dir = tgt_pos - pos.0; + inputs.move_dir = Vec2::from(tgt_pos - pos.0) + .try_normalized() + .unwrap_or(Vec2::zero()) + * 0.01; + inputs.primary.set_state(true); + } + } + + /* match agent { Agent::Wanderer(bearing) => { *bearing += Vec2::new(rand::random::() - 0.5, rand::random::() - 0.5) @@ -74,7 +179,7 @@ impl<'a> System<'a> for Sys { Agent::Pet { target, chaser } => { // Run towards target. if let Some(tgt_pos) = positions.get(*target) { - if let Some(bearing) = chaser.chase(&*terrain, pos.0, tgt_pos.0) { + if let Some(bearing) = chaser.chase(&*terrain, pos.0, tgt_pos.0, 5.0) { inputs.move_dir = Vec2::from(bearing).try_normalized().unwrap_or(Vec2::zero()); inputs.jump.set_state(bearing.z > 1.0); @@ -160,6 +265,7 @@ impl<'a> System<'a> for Sys { } } } + */ debug_assert!(inputs.move_dir.map(|e| !e.is_nan()).reduce_and()); debug_assert!(inputs.look_dir.map(|e| !e.is_nan()).reduce_and()); diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 595bbb3d2a..391348c0ff 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -458,13 +458,19 @@ 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(agent) = alignment_to_agent(&opt_align, entity) { + if let Some(alignment) = parse_alignment(&opt_align) { let amount = opt_amount .and_then(|a| a.parse().ok()) .filter(|x| *x > 0) .unwrap_or(1) .min(10); + let agent = if let comp::Alignment::Npc = alignment { + comp::Agent::default().with_pet(entity) + } else { + comp::Agent::default() + }; + match server.state.read_component_cloned::(entity) { Some(pos) => { for _ in 0..amount { @@ -486,6 +492,7 @@ fn handle_spawn(server: &mut Server, entity: EcsEntity, args: String, action: &C .with(comp::Vel(vel)) .with(comp::MountState::Unmounted) .with(agent.clone()) + .with(alignment) .build(); if let Some(uid) = server.state.ecs().uid_from_entity(new_entity) { @@ -576,11 +583,11 @@ fn handle_help(server: &mut Server, entity: EcsEntity, _args: String, _action: & } } -fn alignment_to_agent(alignment: &str, target: EcsEntity) -> Option { +fn parse_alignment(alignment: &str) -> Option { match alignment { - "hostile" => Some(comp::Agent::enemy()), - "friendly" => Some(comp::Agent::pet(target)), - // passive? + "wild" => Some(comp::Alignment::Wild), + "enemy" => Some(comp::Alignment::Enemy), + "npc" => Some(comp::Alignment::Npc), _ => None, } } diff --git a/server/src/lib.rs b/server/src/lib.rs index 9836380731..e5d1ad20ad 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -133,6 +133,7 @@ impl Server { #[cfg(not(feature = "worldgen"))] let world = World::generate(settings.world_seed); + #[cfg(not(feature = "worldgen"))] let map = vec![0]; #[cfg(feature = "worldgen")] @@ -299,6 +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::Inventory::default()); state.write_component(entity, comp::InventoryUpdate); // Make sure physics are accepted. @@ -834,12 +836,14 @@ impl Server { stats, body, agent, + alignment, scale, } => { state .create_npc(pos, stats, body) .with(agent) .with(scale) + .with(alignment) .build(); } @@ -1228,6 +1232,7 @@ impl StateExt for State { .with(comp::Controller::default()) .with(body) .with(stats) + .with(comp::Alignment::Npc) .with(comp::Energy::new(500)) .with(comp::Gravity(1.0)) .with(comp::CharacterState::default()) diff --git a/server/src/sys/terrain.rs b/server/src/sys/terrain.rs index ab96fa7d25..d2ba658c2a 100644 --- a/server/src/sys/terrain.rs +++ b/server/src/sys/terrain.rs @@ -173,7 +173,8 @@ impl<'a> System<'a> for Sys { pos: Pos(npc.pos), stats, body, - agent: comp::Agent::enemy(), + alignment: comp::Alignment::Enemy, + agent: comp::Agent::default(), scale: comp::Scale(scale), }) }