Better neutral AI, initial waypoint objects

This commit is contained in:
Joshua Barretto 2020-01-25 02:15:15 +00:00
parent 9d7efad526
commit 11193a692a
18 changed files with 241 additions and 204 deletions

BIN
assets/voxygen/voxel/object/campfire_lit.vox (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -119,8 +119,8 @@ where
}
pub enum PathResult<T> {
None,
Exhausted,
None(Path<T>),
Exhausted(Path<T>),
Path(Path<T>),
Pending,
}
@ -134,6 +134,7 @@ pub struct Astar<S: Clone + Eq + Hash> {
cheapest_scores: HashMap<S, f32>,
final_scores: HashMap<S, f32>,
visited: HashSet<S>,
lowest_cost: Option<S>,
}
impl<S: Clone + Eq + Hash> Astar<S> {
@ -150,6 +151,7 @@ impl<S: Clone + Eq + Hash> Astar<S> {
cheapest_scores: std::iter::once((start.clone(), 0.0)).collect(),
final_scores: std::iter::once((start.clone(), heuristic(&start))).collect(),
visited: std::iter::once(start).collect(),
lowest_cost: None,
}
}
@ -169,6 +171,7 @@ impl<S: Clone + Eq + Hash> Astar<S> {
if satisfied(&node) {
return PathResult::Path(self.reconstruct_path_to(node));
} else {
self.lowest_cost = Some(node.clone());
for neighbor in neighbors(&node) {
let node_cheapest = self.cheapest_scores.get(&node).unwrap_or(&f32::MAX);
let neighbor_cheapest =
@ -191,14 +194,24 @@ impl<S: Clone + Eq + Hash> Astar<S> {
}
}
} else {
return PathResult::None;
return PathResult::None(
self.lowest_cost
.clone()
.map(|lc| self.reconstruct_path_to(lc))
.unwrap_or_default(),
);
}
self.iter += 1
}
if self.iter >= self.max_iters {
PathResult::Exhausted
PathResult::Exhausted(
self.lowest_cost
.clone()
.map(|lc| self.reconstruct_path_to(lc))
.unwrap_or_default(),
)
} else {
PathResult::Pending
}

View File

@ -26,9 +26,10 @@ impl Component for Alignment {
#[derive(Clone, Debug, Default)]
pub struct Agent {
pub chaser: Chaser,
pub target: Option<EcsEntity>,
pub target: Option<(EcsEntity, f64)>,
pub owner: Option<EcsEntity>,
pub patrol_origin: Option<Vec3<f32>>,
pub wander_pos: Option<Vec3<f32>>,
}
impl Agent {
@ -36,6 +37,11 @@ impl Agent {
self.owner = Some(owner);
self
}
pub fn with_patrol_origin(mut self, origin: Vec3<f32>) -> Self {
self.patrol_origin = Some(origin);
self
}
}
impl Component for Agent {

View File

@ -54,6 +54,7 @@ pub enum Body {
CraftingBench = 48,
BoltFire = 49,
ArrowSnake = 50,
CampfireLit = 51,
}
impl Body {
@ -63,7 +64,7 @@ impl Body {
}
}
const ALL_OBJECTS: [Body; 50] = [
const ALL_OBJECTS: [Body; 51] = [
Body::Arrow,
Body::Bomb,
Body::Scarecrow,
@ -82,6 +83,7 @@ const ALL_OBJECTS: [Body; 50] = [
Body::Pumpkin4,
Body::Pumpkin5,
Body::Campfire,
Body::CampfireLit,
Body::LanternGround,
Body::LanternGroundOpen,
Body::LanternStanding,

View File

@ -20,3 +20,16 @@ impl Waypoint {
impl Component for Waypoint {
type Storage = FlaggedStorage<Self, IDVStorage<Self>>;
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct WaypointArea(f32);
impl Component for WaypointArea {
type Storage = FlaggedStorage<Self, IDVStorage<Self>>;
}
impl Default for WaypointArea {
fn default() -> Self {
Self(5.0)
}
}

View File

@ -30,7 +30,7 @@ pub use energy::{Energy, EnergySource};
pub use inputs::CanBuild;
pub use inventory::{item, Inventory, InventoryUpdate, Item, ItemKind};
pub use last::Last;
pub use location::Waypoint;
pub use location::{Waypoint, WaypointArea};
pub use phys::{ForceUpdate, Gravity, Mass, Ori, PhysicsState, Pos, Scale, Sticky, Vel};
pub use player::Player;
pub use projectile::Projectile;

View File

@ -105,6 +105,7 @@ pub enum ServerEvent {
alignment: comp::Alignment,
scale: comp::Scale,
},
CreateWaypoint(Vec3<f32>),
ClientDisconnect(EcsEntity),
ChunkRequest(EcsEntity, Vec2<i32>),
ChatCmd(EcsEntity, String),

View File

@ -1,11 +1,24 @@
use vek::*;
pub struct NpcInfo {
pub enum EntityKind {
Enemy,
Boss,
Waypoint,
}
pub struct EntityInfo {
pub pos: Vec3<f32>,
pub boss: bool,
pub kind: EntityKind,
}
#[derive(Default)]
pub struct ChunkSupplement {
pub npcs: Vec<NpcInfo>,
pub entities: Vec<EntityInfo>,
}
impl ChunkSupplement {
pub fn with_entity(mut self, entity: EntityInfo) -> Self {
self.entities.push(entity);
self
}
}

View File

@ -9,11 +9,19 @@ use vek::*;
// Path
#[derive(Default, Clone, Debug)]
#[derive(Clone, Debug)]
pub struct Path<T> {
nodes: Vec<T>,
}
impl<T> Default for Path<T> {
fn default() -> Self {
Self {
nodes: Vec::default(),
}
}
}
impl<T> FromIterator<T> for Path<T> {
fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
Self {
@ -134,7 +142,7 @@ impl Chaser {
self.route = find_path(&mut self.astar, vol, pos, tgt).into();
}
Some(tgt - pos)
Some((tgt - pos) * Vec3::new(1.0, 1.0, 0.0))
}
}
}
@ -225,10 +233,14 @@ where
*astar = None;
path
}
PathResult::Pending => Path::default(),
_ => {
PathResult::None(path) => {
*astar = None;
Path::default()
path
}
PathResult::Exhausted(path) => {
*astar = None;
path
}
PathResult::Pending => Path::default(),
}
}

View File

@ -142,6 +142,7 @@ impl State {
ecs.register::<comp::Last<comp::CharacterState>>();
ecs.register::<comp::Agent>();
ecs.register::<comp::Alignment>();
ecs.register::<comp::WaypointArea>();
ecs.register::<comp::ForceUpdate>();
ecs.register::<comp::InventoryUpdate>();
ecs.register::<comp::Admin>();

View File

@ -4,6 +4,7 @@ use crate::{
self, Agent, Alignment, CharacterState, Controller, MountState, MovementState::Glide, Pos,
Stats,
},
state::Time,
sync::{Uid, UidAllocator},
};
use rand::{seq::SliceRandom, thread_rng, Rng};
@ -18,6 +19,7 @@ pub struct Sys;
impl<'a> System<'a> for Sys {
type SystemData = (
Read<'a, UidAllocator>,
Read<'a, Time>,
Entities<'a>,
ReadStorage<'a, Pos>,
ReadStorage<'a, Stats>,
@ -33,6 +35,7 @@ impl<'a> System<'a> for Sys {
&mut self,
(
uid_allocator,
time,
entities,
positions,
stats,
@ -73,21 +76,25 @@ 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 PATROL_DIST: f32 = 32.0;
const SIGHT_DIST: f32 = 24.0;
const MIN_ATTACK_DIST: f32 = 3.25;
const CHASE_TIME_MIN: f64 = 4.0;
let mut chase_tgt = None;
let mut choose_target = false;
let mut new_target = None;
if let Some(target) = agent.target {
if let Some((target, aggro_time)) = 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 if pos.0.distance(tgt_pos.0) < SIGHT_DIST
|| (time.0 - aggro_time) < CHASE_TIME_MIN
{
chase_tgt = Some((tgt_pos.0, 1.5, true))
} else {
// Lose sight of enemies
choose_target = true;
@ -95,19 +102,22 @@ impl<'a> System<'a> for Sys {
} else {
choose_target = true;
}
} else if let Some(owner) = agent.owner {
}
// Return to owner
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() {
if pos.0.distance(tgt_pos.0) > PET_DIST {
// Follow owner
chase_tgt = Some((tgt_pos.0, 6.0, false));
} else {
} else if agent.target.is_none() {
choose_target = thread_rng().gen::<f32>() < 0.02;
}
} else {
agent.owner = None;
}
} else if let Some(patrol_origin) = agent.patrol_origin {
if pos.0.distance(patrol_origin) < PATROL_DIST {
if pos.0.distance(patrol_origin) > PATROL_DIST {
// Return to patrol origin
chase_tgt = Some((patrol_origin, 64.0, false));
}
@ -120,15 +130,16 @@ impl<'a> System<'a> for Sys {
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());
new_target = uid_allocator.retrieve_entity_internal(by.id());
} else if thread_rng().gen::<f32>() < 0.005 {
agent.target = uid_allocator.retrieve_entity_internal(by.id());
new_target = uid_allocator.retrieve_entity_internal(by.id());
}
}
_ => {}
}
}
// Choose a new target
if choose_target {
// Search for new targets
let entities = (&entities, &positions, &stats, alignments.maybe())
@ -144,7 +155,12 @@ impl<'a> System<'a> for Sys {
.map(|(e, _, _, _)| e)
.collect::<Vec<_>>();
agent.target = (&entities).choose(&mut thread_rng()).cloned();
new_target = (&entities).choose(&mut thread_rng()).cloned();
}
// Update target when attack begins
if let Some(tgt) = new_target {
agent.target = Some((tgt, time.0));
}
// Chase target
@ -154,7 +170,7 @@ impl<'a> System<'a> for Sys {
inputs.jump.set_state(bearing.z > 1.0);
}
if pos.0.distance(tgt_pos) < MIN_ATTACK_DIST && aggressive {
if aggressive && pos.0.distance(tgt_pos) < MIN_ATTACK_DIST {
inputs.look_dir = tgt_pos - pos.0;
inputs.move_dir = Vec2::from(tgt_pos - pos.0)
.try_normalized()
@ -162,110 +178,42 @@ impl<'a> System<'a> for Sys {
* 0.01;
inputs.primary.set_state(true);
}
}
/*
match agent {
Agent::Wanderer(bearing) => {
*bearing += Vec2::new(rand::random::<f32>() - 0.5, rand::random::<f32>() - 0.5)
* 0.1
- *bearing * 0.01
- pos.0 * 0.0002;
if bearing.magnitude_squared() > 0.001 {
inputs.move_dir = bearing.normalized();
}
}
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, 5.0) {
// We're not wandering
agent.wander_pos = None;
} else {
if let Some(wander_pos) = agent.wander_pos {
if pos.0.distance(wander_pos) < 4.0 {
agent.wander_pos = None;
} else {
if let Some(bearing) = agent.chaser.chase(&*terrain, pos.0, wander_pos, 3.0)
{
inputs.move_dir =
Vec2::from(bearing).try_normalized().unwrap_or(Vec2::zero());
Vec2::from(bearing).try_normalized().unwrap_or(Vec2::zero()) * 0.5;
inputs.jump.set_state(bearing.z > 1.0);
}
} else {
inputs.move_dir = Vec2::zero();
}
}
Agent::Enemy { bearing, target } => {
const SIGHT_DIST: f32 = 18.0;
const MIN_ATTACK_DIST: f32 = 3.25;
let mut choose_new = false;
if let Some((Some(target_pos), Some(target_stats), Some(target_character))) =
target.map(|target| {
(
positions.get(target),
stats.get(target),
character_states.get(target),
)
})
{
inputs.look_dir = target_pos.0 - pos.0;
let dist = Vec2::<f32>::from(target_pos.0 - pos.0).magnitude();
if target_stats.is_dead {
choose_new = true;
} else if dist < 0.001 {
// Probably can only happen when entities are at a different z-level
// since at the same level repulsion would keep them apart.
// Distinct from the first if block since we may want to change the
// behavior for this case.
choose_new = true;
} else if dist < MIN_ATTACK_DIST {
// Fight (and slowly move closer)
inputs.move_dir =
Vec2::<f32>::from(target_pos.0 - pos.0).normalized() * 0.01;
inputs.primary.set_state(true);
} else if dist < SIGHT_DIST {
inputs.move_dir =
Vec2::<f32>::from(target_pos.0 - pos.0).normalized() * 0.96;
if rand::random::<f32>() < 0.02 {
inputs.roll.set_state(true);
}
if target_character.movement == Glide && target_pos.0.z > pos.0.z + 5.0
{
inputs.glide.set_state(true);
inputs.jump.set_state(true);
}
// Choose new wander position
if agent.wander_pos.is_none() || thread_rng().gen::<f32>() < 0.005 {
agent.wander_pos = if thread_rng().gen::<f32>() < 0.5 {
let max_dist = if agent.owner.is_some() {
PET_DIST
} else {
choose_new = true;
}
} else {
*bearing +=
Vec2::new(rand::random::<f32>() - 0.5, rand::random::<f32>() - 0.5)
* 0.1
- *bearing * 0.005;
inputs.move_dir = if bearing.magnitude_squared() > 0.001 {
bearing.normalized()
} else {
Vec2::zero()
PATROL_DIST
};
choose_new = true;
}
if choose_new && rand::random::<f32>() < 0.1 {
let entities = (&entities, &positions, &stats)
.join()
.filter(|(e, e_pos, e_stats)| {
(e_pos.0 - pos.0).magnitude() < SIGHT_DIST
&& *e != entity
&& !e_stats.is_dead
})
.map(|(e, _, _)| e)
.collect::<Vec<_>>();
let mut rng = thread_rng();
*target = (&entities).choose(&mut rng).cloned();
}
Some(
agent
.patrol_origin
.unwrap_or(pos.0)
.map(|e| e + (thread_rng().gen::<f32>() - 0.5) * max_dist),
)
} else {
None
};
}
}
*/
debug_assert!(inputs.move_dir.map(|e| !e.is_nan()).reduce_and());
debug_assert!(inputs.look_dir.map(|e| !e.is_nan()).reduce_and());

View File

@ -9,7 +9,7 @@ worldgen = ["server/worldgen"]
default = ["worldgen"]
[dependencies]
server = { package = "veloren-server", path = "../server" }
server = { package = "veloren-server", path = "../server", default-features = false }
common = { package = "veloren-common", path = "../common" }
log = "0.4.8"

View File

@ -657,6 +657,7 @@ fn handle_object(server: &mut Server, entity: EcsEntity, args: String, _action:
Ok("pumpkin_4") => comp::object::Body::Pumpkin4,
Ok("pumpkin_5") => comp::object::Body::Pumpkin5,
Ok("campfire") => comp::object::Body::Campfire,
Ok("campfire_lit") => comp::object::Body::CampfireLit,
Ok("lantern_ground") => comp::object::Body::LanternGround,
Ok("lantern_ground_open") => comp::object::Body::LanternGroundOpen,
Ok("lantern_2") => comp::object::Body::LanternStanding2,

View File

@ -847,6 +847,17 @@ impl Server {
.build();
}
ServerEvent::CreateWaypoint(pos) => {
self.create_object(comp::Pos(pos), comp::object::Body::CampfireLit)
.with(comp::LightEmitter {
offset: Vec3::unit_z() * 0.5,
col: Rgb::new(1.0, 0.65, 0.2),
strength: 2.0,
})
.with(comp::WaypointArea::default())
.build();
}
ServerEvent::ClientDisconnect(entity) => {
// Tell other clients to remove from player list
if let (Some(uid), Some(_)) = (

View File

@ -4,6 +4,7 @@ use common::{
assets,
comp::{self, item, Player, Pos},
event::{EventBus, ServerEvent},
generation::EntityKind,
msg::ServerMsg,
state::TerrainChanges,
terrain::TerrainGrid,
@ -95,88 +96,92 @@ impl<'a> System<'a> for Sys {
}
// Handle chunk supplement
for npc in supplement.npcs {
const SPAWN_NPCS: &'static [fn() -> (String, comp::Body, Option<comp::Item>)] = &[
(|| {
(
"Traveler".into(),
comp::Body::Humanoid(comp::humanoid::Body::random()),
Some(assets::load_expect_cloned("common.items.weapons.staff_1")),
)
}) as _,
(|| {
(
"Wolf".into(),
comp::Body::QuadrupedMedium(comp::quadruped_medium::Body::random()),
None,
)
}) as _,
(|| {
(
"Duck".into(),
comp::Body::BirdMedium(comp::bird_medium::Body::random()),
None,
)
}) as _,
(|| {
(
"Rat".into(),
comp::Body::Critter(comp::critter::Body::random()),
None,
)
}) as _,
(|| {
(
"Pig".into(),
comp::Body::QuadrupedSmall(comp::quadruped_small::Body::random()),
None,
)
}),
];
let (name, mut body, main) = SPAWN_NPCS
.choose(&mut rand::thread_rng())
.expect("SPAWN_NPCS is nonempty")(
);
let mut stats = comp::Stats::new(name, body, main);
for entity in supplement.entities {
if let EntityKind::Waypoint = entity.kind {
server_emitter.emit(ServerEvent::CreateWaypoint(entity.pos));
} else {
const SPAWN_NPCS: &'static [fn() -> (String, comp::Body, Option<comp::Item>)] = &[
(|| {
(
"Traveler".into(),
comp::Body::Humanoid(comp::humanoid::Body::random()),
Some(assets::load_expect_cloned("common.items.weapons.staff_1")),
)
}) as _,
(|| {
(
"Wolf".into(),
comp::Body::QuadrupedMedium(comp::quadruped_medium::Body::random()),
None,
)
}) as _,
(|| {
(
"Duck".into(),
comp::Body::BirdMedium(comp::bird_medium::Body::random()),
None,
)
}) as _,
(|| {
(
"Rat".into(),
comp::Body::Critter(comp::critter::Body::random()),
None,
)
}) as _,
(|| {
(
"Pig".into(),
comp::Body::QuadrupedSmall(comp::quadruped_small::Body::random()),
None,
)
}),
];
let (name, mut body, main) = SPAWN_NPCS
.choose(&mut rand::thread_rng())
.expect("SPAWN_NPCS is nonempty")(
);
let mut stats = comp::Stats::new(name, body, main);
let mut scale = 1.0;
let mut scale = 1.0;
// TODO: Remove this and implement scaling or level depending on stuff like species instead
stats.level.set_level(rand::thread_rng().gen_range(1, 4));
// TODO: Remove this and implement scaling or level depending on stuff like species instead
stats.level.set_level(rand::thread_rng().gen_range(1, 4));
if npc.boss {
if rand::random::<f32>() < 0.8 {
let hbody = comp::humanoid::Body::random();
body = comp::Body::Humanoid(hbody);
stats = comp::Stats::new(
"Fearless Wanderer".to_string(),
body,
Some(assets::load_expect_cloned("common.items.weapons.hammer_1")),
);
if let EntityKind::Boss = entity.kind {
if rand::random::<f32>() < 0.8 {
let hbody = comp::humanoid::Body::random();
body = comp::Body::Humanoid(hbody);
stats = comp::Stats::new(
"Fearless Wanderer".to_string(),
body,
Some(assets::load_expect_cloned("common.items.weapons.hammer_1")),
);
}
stats.level.set_level(rand::thread_rng().gen_range(8, 15));
scale = 2.0 + rand::random::<f32>();
}
stats.level.set_level(rand::thread_rng().gen_range(8, 15));
scale = 2.0 + rand::random::<f32>();
}
stats.update_max_hp();
stats
.health
.set_to(stats.health.maximum(), comp::HealthSource::Revive);
if let Some(item::Item {
kind: item::ItemKind::Tool { power, .. },
..
}) = &mut stats.equipment.main
{
*power = stats.level.level() * 3;
stats.update_max_hp();
stats
.health
.set_to(stats.health.maximum(), comp::HealthSource::Revive);
if let Some(item::Item {
kind: item::ItemKind::Tool { power, .. },
..
}) = &mut stats.equipment.main
{
*power = stats.level.level() * 3;
}
server_emitter.emit(ServerEvent::CreateNpc {
pos: Pos(entity.pos),
stats,
body,
alignment: comp::Alignment::Enemy,
agent: comp::Agent::default().with_patrol_origin(entity.pos),
scale: comp::Scale(scale),
})
}
server_emitter.emit(ServerEvent::CreateNpc {
pos: Pos(npc.pos),
stats,
body,
alignment: comp::Alignment::Enemy,
agent: comp::Agent::default(),
scale: comp::Scale(scale),
})
}
}

View File

@ -1,5 +1,5 @@
use common::{
generation::{ChunkSupplement, NpcInfo},
generation::{ChunkSupplement, EntityInfo, EntityKind},
terrain::{Block, BlockKind, TerrainChunk, TerrainChunkMeta, TerrainChunkSize},
vol::{ReadVol, RectVolSize, Vox, WriteVol},
};
@ -37,7 +37,10 @@ impl World {
Block::empty(),
TerrainChunkMeta::void(),
),
ChunkSupplement::default(),
ChunkSupplement::default().with_entity(EntityInfo {
pos: Vec3::<f32>::from(chunk_pos.map(|e| e as f32 * 32.0)) + Vec3::unit_z() * 256.0,
kind: EntityKind::Waypoint,
}),
))
}
}

View File

@ -1713,6 +1713,7 @@ pub fn mesh_object(obj: object::Body) -> Mesh<FigurePipeline> {
Body::Pumpkin4 => ("object.pumpkin_4", Vec3::new(-5.0, -4.0, 0.0)),
Body::Pumpkin5 => ("object.pumpkin_5", Vec3::new(-4.0, -5.0, 0.0)),
Body::Campfire => ("object.campfire", Vec3::new(-9.0, -10.0, 0.0)),
Body::CampfireLit => ("object.campfire_lit", Vec3::new(-9.0, -10.0, 0.0)),
Body::LanternGround => ("object.lantern_ground", Vec3::new(-3.5, -3.5, 0.0)),
Body::LanternGroundOpen => ("object.lantern_ground_open", Vec3::new(-3.5, -3.5, 0.0)),
Body::LanternStanding => ("object.lantern_standing", Vec3::new(-7.5, -3.5, 0.0)),

View File

@ -19,7 +19,7 @@ use crate::{
util::Sampler,
};
use common::{
generation::{ChunkSupplement, NpcInfo},
generation::{ChunkSupplement, EntityInfo, EntityKind},
terrain::{Block, BlockKind, TerrainChunk, TerrainChunkMeta, TerrainChunkSize},
vol::{ReadVol, RectVolSize, Vox, WriteVol},
};
@ -150,13 +150,17 @@ impl World {
const SPAWN_RATE: f32 = 0.1;
const BOSS_RATE: f32 = 0.03;
let supplement = ChunkSupplement {
npcs: if rand::thread_rng().gen::<f32>() < SPAWN_RATE
entities: if rand::thread_rng().gen::<f32>() < SPAWN_RATE
&& sim_chunk.chaos < 0.5
&& !sim_chunk.is_underwater
{
vec![NpcInfo {
vec![EntityInfo {
pos: gen_entity_pos(),
boss: rand::thread_rng().gen::<f32>() < BOSS_RATE,
kind: if rand::thread_rng().gen::<f32>() < BOSS_RATE {
EntityKind::Boss
} else {
EntityKind::Enemy
},
}]
} else {
Vec::new()