Merge branch 'zesterer/waystones' into 'master'

Pathfinding, better AI, waypoints

See merge request veloren/veloren!753
This commit is contained in:
Joshua Barretto 2020-01-26 13:50:30 +00:00
commit 6b4a09af30
33 changed files with 1007 additions and 597 deletions

View File

@ -46,6 +46,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added fullscreen and window size to settings so that they can be persisted
- Added coverage based scaling for pixel art
- 28 new mobs
- Added waypoints
- Added pathfinding to NPCs
- Overhauled NPC AI
- Pets now attack enemies and defend their owners
### Changed

10
Cargo.lock generated
View File

@ -2329,6 +2329,7 @@ dependencies = [
"rand_chacha 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
"rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"rand_pcg 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@ -2435,6 +2436,14 @@ dependencies = [
"rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "rand_pcg"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "rand_xorshift"
version = "0.1.1"
@ -3858,6 +3867,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum rand_os 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071"
"checksum rand_os 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a788ae3edb696cfcba1c19bfd388cc4b8c21f8a408432b199c072825084da58a"
"checksum rand_pcg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44"
"checksum rand_pcg 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429"
"checksum rand_xorshift 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c"
"checksum rand_xoshiro 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0e18c91676f670f6f0312764c759405f13afb98d5d73819840cf72a518487bff"
"checksum raw-window-handle 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9db80d08d3ed847ce4fb3def46de0af4bfb6155bd09bd6eaf28b5ac72541c1f1"

View File

@ -112,9 +112,7 @@ Levels/Items are not saved yet."#,
"hud.press_key_to_toggle_debug_info_fmt": "Press {key} to toogle debug info",
// Respawn message
"hud.press_key_to_respawn": r#"Press {key} to respawn at your Waypoint.
Press Enter, type in /waypoint and confirm to set it here."#,
"hud.press_key_to_respawn": r#"Press {key} to respawn at the last campfire you visited."#,
// Welcome message
"hud.welcome": r#"Welcome to the Veloren Alpha!,

View File

@ -66,7 +66,7 @@ float shadow_at(vec3 wpos, vec3 wnorm) {
vec3 diff = shadow_pos - wpos;
if (diff.z >= 0.0) {
diff.z = sign(diff.z) * 0.1;
diff.z = -sign(diff.z) * diff.z * 0.1;
}
float shade = max(pow(diff.x * diff.x + diff.y * diff.y + diff.z * diff.z, 0.25) / pow(radius * radius * 0.5, 0.25), 0.5);

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

Binary file not shown.

View File

@ -1,3 +1,4 @@
use crate::path::Path;
use core::cmp::Ordering::Equal;
use hashbrown::{HashMap, HashSet};
use std::cmp::Ordering;
@ -5,7 +6,7 @@ use std::collections::BinaryHeap;
use std::f32;
use std::hash::Hash;
#[derive(Copy, Clone)]
#[derive(Copy, Clone, Debug)]
pub struct PathEntry<S> {
cost: f32,
node: S,
@ -33,79 +34,113 @@ impl<S: Eq> PartialOrd for PathEntry<S> {
}
}
fn reconstruct_path<S>(came_from: &HashMap<S, S>, target: &S) -> Vec<S>
where
S: Clone + Eq + Hash,
{
let mut path = Vec::new();
path.push(target.to_owned());
let mut cur_node = target;
while let Some(node) = came_from.get(cur_node) {
path.push(node.to_owned());
cur_node = node;
}
path
pub enum PathResult<T> {
None(Path<T>),
Exhausted(Path<T>),
Path(Path<T>),
Pending,
}
pub fn astar<S, I>(
initial: S,
target: S,
mut heuristic: impl FnMut(&S, &S) -> f32,
mut neighbors: impl FnMut(&S) -> I,
mut transition_cost: impl FnMut(&S, &S) -> f32,
) -> Option<Vec<S>>
where
S: Clone + Eq + Hash,
I: IntoIterator<Item = S>,
{
// Set of discovered nodes so far
let mut potential_nodes = BinaryHeap::new();
potential_nodes.push(PathEntry {
cost: 0.0f32,
node: initial.clone(),
});
#[derive(Clone, Debug)]
pub struct Astar<S: Clone + Eq + Hash> {
iter: usize,
max_iters: usize,
potential_nodes: BinaryHeap<PathEntry<S>>,
came_from: HashMap<S, S>,
cheapest_scores: HashMap<S, f32>,
final_scores: HashMap<S, f32>,
visited: HashSet<S>,
lowest_cost: Option<S>,
}
// For entry e, contains the cheapest node preceding it on the known path from start to e
let mut came_from = HashMap::new();
// Contains cheapest cost from 'initial' to the current entry
let mut cheapest_scores = HashMap::new();
cheapest_scores.insert(initial.clone(), 0.0f32);
// Contains cheapest score to get to node + heuristic to the end, for an entry
let mut final_scores = HashMap::new();
final_scores.insert(initial.clone(), heuristic(&initial, &target));
// Set of nodes we have already visited
let mut visited = HashSet::new();
visited.insert(initial.clone());
while let Some(PathEntry { node: current, .. }) = potential_nodes.pop() {
if current == target {
return Some(reconstruct_path(&came_from, &current));
impl<S: Clone + Eq + Hash> Astar<S> {
pub fn new(max_iters: usize, start: S, heuristic: impl FnOnce(&S) -> f32) -> Self {
Self {
max_iters,
iter: 0,
potential_nodes: std::iter::once(PathEntry {
cost: 0.0,
node: start.clone(),
})
.collect(),
came_from: HashMap::default(),
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,
}
}
let current_neighbors = neighbors(&current);
for neighbor in current_neighbors {
let current_cheapest_score = cheapest_scores.get(&current).unwrap_or(&f32::MAX);
let neighbor_cheapest_score = cheapest_scores.get(&neighbor).unwrap_or(&f32::MAX);
let score = current_cheapest_score + transition_cost(&current, &neighbor);
if score < *neighbor_cheapest_score {
// Path to the neighbor is better than anything yet recorded
came_from.insert(neighbor.to_owned(), current.to_owned());
cheapest_scores.insert(neighbor.clone(), score);
let neighbor_score = score + heuristic(&neighbor, &target);
final_scores.insert(neighbor.clone(), neighbor_score);
pub fn poll<I>(
&mut self,
iters: usize,
mut heuristic: impl FnMut(&S) -> f32,
mut neighbors: impl FnMut(&S) -> I,
mut transition: impl FnMut(&S, &S) -> f32,
mut satisfied: impl FnMut(&S) -> bool,
) -> PathResult<S>
where
I: Iterator<Item = S>,
{
let iter_limit = self.max_iters.min(self.iter + iters);
while self.iter < iter_limit {
if let Some(PathEntry { node, .. }) = self.potential_nodes.pop() {
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 =
self.cheapest_scores.get(&neighbor).unwrap_or(&f32::MAX);
if visited.insert(neighbor.clone()) {
potential_nodes.push(PathEntry {
node: neighbor.clone(),
cost: neighbor_score,
});
let cost = node_cheapest + transition(&node, &neighbor);
if cost < *neighbor_cheapest {
self.came_from.insert(neighbor.clone(), node.clone());
self.cheapest_scores.insert(neighbor.clone(), cost);
let neighbor_cost = cost + heuristic(&neighbor);
self.final_scores.insert(neighbor.clone(), neighbor_cost);
if self.visited.insert(neighbor.clone()) {
self.potential_nodes.push(PathEntry {
node: neighbor.clone(),
cost: neighbor_cost,
});
}
}
}
}
} else {
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(
self.lowest_cost
.clone()
.map(|lc| self.reconstruct_path_to(lc))
.unwrap_or_default(),
)
} else {
PathResult::Pending
}
}
None
fn reconstruct_path_to(&mut self, end: S) -> Path<S> {
let mut path = vec![end.clone()];
let mut cnode = &end;
while let Some(node) = self.came_from.get(cnode) {
path.push(node.clone());
cnode = node;
}
path.into_iter().rev().collect()
}
}

View File

@ -1,33 +1,76 @@
use crate::pathfinding::WorldPath;
use crate::path::Chaser;
use specs::{Component, Entity as EcsEntity};
use specs_idvs::IDVStorage;
use vek::*;
#[derive(Clone, Debug)]
pub enum Agent {
Wanderer(Vec2<f32>),
Pet {
target: EcsEntity,
offset: Vec2<f32>,
},
Enemy {
bearing: Vec2<f32>,
target: Option<EcsEntity>,
},
Traveler {
path: WorldPath,
},
#[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) => false,
_ => self != other,
}
}
}
impl Component for Alignment {
type Storage = IDVStorage<Self>;
}
#[derive(Clone, Debug, Default)]
pub struct Agent {
pub owner: Option<EcsEntity>,
pub patrol_origin: Option<Vec3<f32>>,
pub activity: Activity,
}
impl Agent {
pub fn enemy() -> Self {
Agent::Enemy {
bearing: Vec2::zero(),
target: None,
}
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 {
self.patrol_origin = Some(origin);
self
}
}
impl Component for Agent {
type Storage = IDVStorage<Self>;
}
#[derive(Clone, Debug)]
pub enum Activity {
Idle(Vec2<f32>),
Follow(EcsEntity, Chaser),
Attack(EcsEntity, Chaser, f64),
}
impl Activity {
pub fn is_follow(&self) -> bool {
match self {
Activity::Follow(_, _) => true,
_ => false,
}
}
pub fn is_attack(&self) -> bool {
match self {
Activity::Attack(_, _, _) => true,
_ => false,
}
}
}
impl Default for Activity {
fn default() -> Self {
Activity::Idle(Vec2::zero())
}
}

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,22 @@ impl Waypoint {
impl Component for Waypoint {
type Storage = FlaggedStorage<Self, IDVStorage<Self>>;
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct WaypointArea(f32);
impl WaypointArea {
pub fn radius(&self) -> f32 {
self.0
}
}
impl Component for WaypointArea {
type Storage = FlaggedStorage<Self, IDVStorage<Self>>;
}
impl Default for WaypointArea {
fn default() -> Self {
Self(5.0)
}
}

View File

@ -1,5 +1,5 @@
mod admin;
mod agent;
pub mod agent;
mod body;
mod character_state;
mod controller;
@ -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,
@ -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

@ -102,8 +102,10 @@ pub enum ServerEvent {
stats: comp::Stats,
body: comp::Body,
agent: comp::Agent,
alignment: comp::Alignment,
scale: comp::Scale,
},
CreateWaypoint(Vec3<f32>),
ClientDisconnect(EcsEntity),
ChunkRequest(EcsEntity, Vec2<i32>),
ChatCmd(EcsEntity, String),

24
common/src/generation.rs Normal file
View File

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

View File

@ -45,7 +45,7 @@ impl ChunkPath {
pub fn chunk_get_neighbors<V: RectRasterableVol + ReadVol + Debug>(
_vol: &VolGrid2d<V>,
pos: &Vec2<i32>,
) -> impl IntoIterator<Item = Vec2<i32>> {
) -> impl Iterator<Item = Vec2<i32>> {
let directions = vec![
Vec2::new(1, 0), // Right chunk
Vec2::new(-1, 0), // Left chunk
@ -53,7 +53,14 @@ impl ChunkPath {
Vec2::new(0, -1), // Bottom chunk
];
let neighbors: Vec<Vec2<i32>> = directions.into_iter().map(|dir| dir + pos).collect();
let mut neighbors = Vec::new();
for x in -2..3 {
for y in -2..3 {
neighbors.push(pos + Vec2::new(x, y));
}
}
//let neighbors: Vec<Vec2<i32>> = directions.into_iter().map(|dir| dir + pos).collect();
neighbors.into_iter()
}
@ -61,7 +68,7 @@ impl ChunkPath {
&mut self,
vol: &VolGrid2d<V>,
pos: Vec3<i32>,
) -> impl IntoIterator<Item = Vec3<i32>> {
) -> impl Iterator<Item = Vec3<i32>> {
let directions = vec![
Vec3::new(0, 1, 0), // Forward
Vec3::new(0, 1, 1), // Forward upward
@ -102,7 +109,7 @@ impl ChunkPath {
.any(|new_pos| new_pos.cmpeq(&vol.pos_key(pos)).iter().all(|e| *e));
}
_ => {
println!("No chunk path");
//println!("No chunk path");
}
}
return is_walkable_position && is_within_chunk;
@ -110,11 +117,11 @@ impl ChunkPath {
pub fn get_worldpath<V: RectRasterableVol + ReadVol + Debug>(
&mut self,
vol: &VolGrid2d<V>,
) -> WorldPath {
) -> Result<WorldPath, ()> {
let wp = WorldPath::new(vol, self.from, self.dest, |vol, pos| {
self.worldpath_get_neighbors(vol, *pos)
self.worldpath_get_neighbors(vol, pos)
});
println!("Fetching world path from hierarchical path: {:?}", wp);
//println!("Fetching world path from hierarchical path: {:?}", wp);
wp
}
}

View File

@ -1,6 +1,6 @@
#![deny(unsafe_code)]
#![type_length_limit = "1664759"]
#![feature(trait_alias, arbitrary_enum_discriminant)]
#![feature(trait_alias, arbitrary_enum_discriminant, label_break_value)]
#[macro_use]
extern crate serde_derive;
@ -14,10 +14,10 @@ pub mod comp;
pub mod effect;
pub mod event;
pub mod figure;
pub mod hierarchical;
pub mod generation;
pub mod msg;
pub mod npc;
pub mod pathfinding;
pub mod path;
pub mod ray;
pub mod region;
pub mod state;

268
common/src/path.rs Normal file
View File

@ -0,0 +1,268 @@
use crate::{
astar::{Astar, PathResult},
terrain::Block,
vol::{BaseVol, ReadVol},
};
use rand::{thread_rng, Rng};
use std::iter::FromIterator;
use vek::*;
// Path
#[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 {
nodes: iter.into_iter().collect(),
}
}
}
impl<T> Path<T> {
pub fn len(&self) -> usize {
self.nodes.len()
}
pub fn start(&self) -> Option<&T> {
self.nodes.first()
}
pub fn end(&self) -> Option<&T> {
self.nodes.last()
}
}
// Route: A path that can be progressed along
#[derive(Default, Clone, Debug)]
pub struct Route {
path: Path<Vec3<i32>>,
next_idx: usize,
}
impl From<Path<Vec3<i32>>> for Route {
fn from(path: Path<Vec3<i32>>) -> Self {
Self { path, next_idx: 0 }
}
}
impl Route {
pub fn path(&self) -> &Path<Vec3<i32>> {
&self.path
}
pub fn next(&self) -> Option<Vec3<i32>> {
self.path.nodes.get(self.next_idx).copied()
}
pub fn is_finished(&self) -> bool {
self.next().is_none()
}
pub fn traverse<V>(&mut self, vol: &V, pos: Vec3<f32>) -> Option<Vec3<f32>>
where
V: BaseVol<Vox = Block> + ReadVol,
{
let next = self.next()?;
if vol.get(next).map(|b| b.is_solid()).unwrap_or(false) {
None
} else {
let next_tgt = next.map(|e| e as f32) + Vec3::new(0.5, 0.5, 0.0);
if ((pos - next_tgt) * Vec3::new(1.0, 1.0, 0.3)).magnitude_squared() < 1.0f32.powf(2.0)
{
self.next_idx += 1;
}
Some(next_tgt - pos)
}
}
}
/// A self-contained system that attempts to chase a moving target, only performing pathfinding if necessary
#[derive(Default, Clone, Debug)]
pub struct Chaser {
last_search_tgt: Option<Vec3<f32>>,
route: Route,
astar: Option<Astar<Vec3<i32>>>,
}
impl Chaser {
pub fn chase<V>(
&mut self,
vol: &V,
pos: Vec3<f32>,
tgt: Vec3<f32>,
min_dist: f32,
) -> Option<Vec3<f32>>
where
V: BaseVol<Vox = Block> + ReadVol,
{
let pos_to_tgt = pos.distance(tgt);
if ((pos - tgt) * Vec3::new(1.0, 1.0, 0.3)).magnitude_squared() < min_dist.powf(2.0) {
return None;
}
let bearing = if let Some(end) = self.route.path().end().copied() {
let end_to_tgt = end.map(|e| e as f32).distance(tgt);
if end_to_tgt > pos_to_tgt * 0.3 + 5.0 {
None
} else {
if thread_rng().gen::<f32>() < 0.005 {
// TODO: Only re-calculate route when we're stuck
self.route = Route::default();
}
self.route.traverse(vol, pos)
}
} else {
None
};
// TODO: What happens when we get stuck?
if let Some(bearing) = bearing {
Some(bearing)
} else {
if self
.last_search_tgt
.map(|last_tgt| last_tgt.distance(tgt) > pos_to_tgt * 0.15 + 5.0)
.unwrap_or(true)
{
self.route = find_path(&mut self.astar, vol, pos, tgt).into();
}
Some((tgt - pos) * Vec3::new(1.0, 1.0, 0.0))
}
}
}
fn find_path<V>(
astar: &mut Option<Astar<Vec3<i32>>>,
vol: &V,
startf: Vec3<f32>,
endf: Vec3<f32>,
) -> Path<Vec3<i32>>
where
V: BaseVol<Vox = Block> + ReadVol,
{
let is_walkable = |pos: &Vec3<i32>| {
vol.get(*pos - Vec3::new(0, 0, 1))
.map(|b| b.is_solid())
.unwrap_or(false)
&& vol
.get(*pos + Vec3::new(0, 0, 0))
.map(|b| !b.is_solid())
.unwrap_or(true)
&& vol
.get(*pos + Vec3::new(0, 0, 1))
.map(|b| !b.is_solid())
.unwrap_or(true)
};
let get_walkable_z = |pos| {
let mut z_incr = 0;
for _ in 0..32 {
let test_pos = pos + Vec3::unit_z() * z_incr;
if is_walkable(&test_pos) {
return Some(test_pos);
}
z_incr = -z_incr + if z_incr <= 0 { 1 } else { 0 };
}
None
};
let (start, end) = match (
get_walkable_z(startf.map(|e| e.floor() as i32)),
get_walkable_z(endf.map(|e| e.floor() as i32)),
) {
(Some(start), Some(end)) => (start, end),
_ => return Path::default(),
};
let heuristic = |pos: &Vec3<i32>| (pos.distance_squared(end) as f32).sqrt();
let neighbors = |pos: &Vec3<i32>| {
let pos = *pos;
const DIRS: [Vec3<i32>; 17] = [
Vec3::new(0, 1, 0), // Forward
Vec3::new(0, 1, 1), // Forward upward
Vec3::new(0, 1, 2), // Forward Upwardx2
Vec3::new(0, 1, -1), // Forward downward
Vec3::new(1, 0, 0), // Right
Vec3::new(1, 0, 1), // Right upward
Vec3::new(1, 0, 2), // Right Upwardx2
Vec3::new(1, 0, -1), // Right downward
Vec3::new(0, -1, 0), // Backwards
Vec3::new(0, -1, 1), // Backward Upward
Vec3::new(0, -1, 2), // Backward Upwardx2
Vec3::new(0, -1, -1), // Backward downward
Vec3::new(-1, 0, 0), // Left
Vec3::new(-1, 0, 1), // Left upward
Vec3::new(-1, 0, 2), // Left Upwardx2
Vec3::new(-1, 0, -1), // Left downward
Vec3::new(0, 0, -1), // Downwards
];
let walkable = [
is_walkable(&(pos + Vec3::new(1, 0, 0))),
is_walkable(&(pos + Vec3::new(-1, 0, 0))),
is_walkable(&(pos + Vec3::new(0, 1, 0))),
is_walkable(&(pos + Vec3::new(0, -1, 0))),
];
const DIAGONALS: [(Vec3<i32>, [usize; 2]); 4] = [
(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]),
];
DIRS.iter()
.map(move |dir| pos + dir)
.filter(move |pos| is_walkable(pos))
.chain(
DIAGONALS
.iter()
.filter(move |(dir, [a, b])| {
is_walkable(&(pos + *dir)) && walkable[*a] && walkable[*b]
})
.map(move |(dir, _)| pos + *dir),
)
};
let transition = |_: &Vec3<i32>, _: &Vec3<i32>| 1.0;
let satisfied = |pos: &Vec3<i32>| pos == &end;
let mut new_astar = match astar.take() {
None => Astar::new(20_000, start, heuristic.clone()),
Some(astar) => astar,
};
let path_result = new_astar.poll(60, heuristic, neighbors, transition, satisfied);
*astar = Some(new_astar);
match path_result {
PathResult::Path(path) => {
*astar = None;
path
}
PathResult::None(path) => {
*astar = None;
path
}
PathResult::Exhausted(path) => {
*astar = None;
path
}
PathResult::Pending => Path::default(),
}
}

View File

@ -1,194 +0,0 @@
use crate::comp::{ControllerInputs, Pos};
use crate::{
astar::astar,
vol::{ReadVol, Vox},
};
use vek::*;
#[derive(Clone, Debug, Default)]
pub struct WorldPath {
pub from: Vec3<f32>,
pub dest: Vec3<f32>,
pub path: Option<Vec<Vec3<i32>>>,
}
impl WorldPath {
pub fn new<V: ReadVol, I>(
vol: &V,
from: Vec3<f32>,
dest: Vec3<f32>,
get_neighbors: impl FnMut(&V, &Vec3<i32>) -> I,
) -> Self
where
I: IntoIterator<Item = Vec3<i32>>,
{
let ifrom: Vec3<i32> = Vec3::from(from.map(|e| e.floor() as i32));
let idest: Vec3<i32> = Vec3::from(dest.map(|e| e.floor() as i32));
let path = WorldPath::get_path(vol, ifrom, idest, get_neighbors);
Self { from, dest, path }
}
pub fn get_path<V: ReadVol, I>(
vol: &V,
from: Vec3<i32>,
dest: Vec3<i32>,
mut get_neighbors: impl FnMut(&V, &Vec3<i32>) -> I,
) -> Option<Vec<Vec3<i32>>>
where
I: IntoIterator<Item = Vec3<i32>>,
{
let new_start = WorldPath::get_z_walkable_space(vol, from);
let new_dest = WorldPath::get_z_walkable_space(vol, dest);
if let (Some(new_start), Some(new_dest)) = (new_start, new_dest) {
astar(
new_start,
new_dest,
euclidean_distance,
|pos| get_neighbors(vol, pos),
transition_cost,
)
} else {
None
}
}
fn get_z_walkable_space<V: ReadVol>(vol: &V, pos: Vec3<i32>) -> Option<Vec3<i32>> {
if WorldPath::is_walkable_space(vol, pos) {
return Some(pos);
}
let mut cur_pos_below = pos.clone();
while !WorldPath::is_walkable_space(vol, cur_pos_below) && cur_pos_below.z > 0 {
cur_pos_below.z -= 1;
}
let max_z = 1000;
let mut cur_pos_above = pos.clone();
while !WorldPath::is_walkable_space(vol, cur_pos_above) && cur_pos_above.z <= max_z {
cur_pos_above.z += 1;
}
if cur_pos_below.z > 0 {
Some(cur_pos_below)
} else if cur_pos_above.z < max_z {
Some(cur_pos_above)
} else {
None
}
}
pub fn is_walkable_space<V: ReadVol>(vol: &V, pos: Vec3<i32>) -> bool {
if let (Ok(voxel), Ok(upper_neighbor), Ok(upper_neighbor2)) = (
vol.get(pos),
vol.get(pos + Vec3::new(0, 0, 1)),
vol.get(pos + Vec3::new(0, 0, 2)),
) {
!voxel.is_empty() && upper_neighbor.is_empty() && upper_neighbor2.is_empty()
} else {
false
}
}
pub fn get_neighbors<V: ReadVol>(
vol: &V,
pos: &Vec3<i32>,
) -> impl IntoIterator<Item = Vec3<i32>> {
let directions = vec![
Vec3::new(0, 1, 0), // Forward
Vec3::new(0, 1, 1), // Forward upward
Vec3::new(0, 1, 2), // Forward Upwardx2
Vec3::new(0, 1, -1), // Forward downward
Vec3::new(1, 0, 0), // Right
Vec3::new(1, 0, 1), // Right upward
Vec3::new(1, 0, 2), // Right Upwardx2
Vec3::new(1, 0, -1), // Right downward
Vec3::new(0, -1, 0), // Backwards
Vec3::new(0, -1, 1), // Backward Upward
Vec3::new(0, -1, 2), // Backward Upwardx2
Vec3::new(0, -1, -1), // Backward downward
Vec3::new(-1, 0, 0), // Left
Vec3::new(-1, 0, 1), // Left upward
Vec3::new(-1, 0, 2), // Left Upwardx2
Vec3::new(-1, 0, -1), // Left downward
Vec3::new(0, 0, -1), // Downwards
];
let neighbors: Vec<Vec3<i32>> = directions
.into_iter()
.map(|dir| dir + pos)
.filter(|new_pos| Self::is_walkable_space(vol, *new_pos))
.collect();
neighbors.into_iter()
}
pub fn move_along_path<V: ReadVol>(
&mut self,
vol: &V,
pos: &Pos,
inputs: &mut ControllerInputs,
is_destination: impl Fn(Vec3<i32>, Vec3<i32>) -> bool,
found_destination: impl FnOnce(),
) {
// No path available
if self.path == None {
return;
}
let ipos = pos.0.map(|e| e.floor() as i32);
let idest = self.dest.map(|e| e.floor() as i32);
// We have reached the end of the path
if is_destination(ipos, idest) {
found_destination();
}
if let Some(mut block_path) = self.path.clone() {
if let Some(next_pos) = block_path.clone().last() {
if self.path_is_blocked(vol) {
self.path = WorldPath::get_path(vol, ipos, idest, WorldPath::get_neighbors);
}
if Vec2::<i32>::from(ipos) == Vec2::<i32>::from(*next_pos) {
block_path.pop();
self.path = Some(block_path);
}
let move_dir = Vec2::<i32>::from(next_pos - ipos);
// Move the input towards the next area on the path
inputs.move_dir = Vec2::<f32>::new(move_dir.x as f32, move_dir.y as f32);
// Need to jump to continue
if next_pos.z >= ipos.z + 1 {
inputs.jump.set_state(true);
}
// Need to glide
let min_z_glide_height = 3;
if next_pos.z - min_z_glide_height < ipos.z {
inputs.glide.set_state(true);
}
}
}
}
pub fn path_is_blocked<V: ReadVol>(&self, vol: &V) -> bool {
match self.path.clone() {
Some(path) => path
.iter()
.any(|pos| !WorldPath::is_walkable_space(vol, *pos)),
_ => false,
}
}
}
pub fn euclidean_distance(start: &Vec3<i32>, end: &Vec3<i32>) -> f32 {
start.map(|e| e as f32).distance(end.map(|e| e as f32))
}
pub fn transition_cost(_start: &Vec3<i32>, _end: &Vec3<i32>) -> f32 {
1.0f32
}

View File

@ -141,6 +141,8 @@ impl State {
ecs.register::<comp::Last<comp::Ori>>();
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

@ -1,22 +1,28 @@
use crate::comp::{
Agent, CharacterState, Controller, MountState, MovementState::Glide, Pos, Stats,
};
use crate::hierarchical::ChunkPath;
use crate::pathfinding::WorldPath;
use crate::terrain::TerrainGrid;
use rand::{seq::SliceRandom, thread_rng};
use specs::{Entities, Join, ReadExpect, ReadStorage, System, WriteStorage};
use crate::{
comp::{self, agent::Activity, Agent, Alignment, Controller, MountState, Pos, Stats},
path::Chaser,
state::Time,
sync::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>,
Read<'a, Time>,
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>,
@ -25,19 +31,22 @@ impl<'a> System<'a> for Sys {
fn run(
&mut self,
(
uid_allocator,
time,
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(),
@ -62,149 +71,175 @@ impl<'a> System<'a> for Sys {
let mut inputs = &mut controller.inputs;
match agent {
Agent::Traveler { path } => {
let mut new_path: Option<WorldPath> = None;
let is_destination = |cur_pos: Vec3<i32>, dest: Vec3<i32>| {
Vec2::<i32>::from(cur_pos) == Vec2::<i32>::from(dest)
};
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 MIN_ATTACK_DIST: f32 = 3.25;
let found_destination = || {
const MAX_TRAVEL_DIST: f32 = 200.0;
let new_dest = Vec3::new(rand::random::<f32>(), rand::random::<f32>(), 0.0)
* MAX_TRAVEL_DIST;
new_path = Some(
ChunkPath::new(&*terrain, pos.0, pos.0 + new_dest)
.get_worldpath(&*terrain),
);
};
let mut do_idle = false;
let mut choose_target = false;
path.move_along_path(
&*terrain,
pos,
&mut inputs,
is_destination,
found_destination,
);
if let Some(new_path) = new_path {
*path = new_path;
}
}
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, offset } => {
// Run towards target.
match positions.get(*target) {
Some(tgt_pos) => {
let tgt_pos = tgt_pos.0 + *offset;
if tgt_pos.z > pos.0.z + 1.0 {
inputs.jump.set_state(true);
}
// Move towards the target.
let dist: f32 = Vec2::from(tgt_pos - pos.0).magnitude();
inputs.move_dir = if dist > 5.0 {
Vec2::from(tgt_pos - pos.0).normalized()
} else if dist < 1.5 && dist > 0.001 {
Vec2::from(pos.0 - tgt_pos).normalized()
'activity: {
match &mut agent.activity {
Activity::Idle(bearing) => {
*bearing += Vec2::new(
thread_rng().gen::<f32>() - 0.5,
thread_rng().gen::<f32>() - 0.5,
) * 0.1
- *bearing * 0.01
- if let Some(patrol_origin) = agent.patrol_origin {
Vec2::<f32>::from(pos.0 - patrol_origin) * 0.0002
} else {
Vec2::zero()
};
}
_ => inputs.move_dir = Vec2::zero(),
}
// Change offset occasionally.
if rand::random::<f32>() < 0.003 {
*offset =
Vec2::new(rand::random::<f32>() - 0.5, rand::random::<f32>() - 0.5)
* 10.0;
if bearing.magnitude_squared() > 0.25f32.powf(2.0) {
inputs.move_dir = bearing.normalized() * 0.65;
}
// Sometimes try searching for new targets
if thread_rng().gen::<f32>() < 0.1 {
choose_target = true;
}
}
Activity::Follow(target, chaser) => {
if let (Some(tgt_pos), _tgt_stats) =
(positions.get(*target), stats.get(*target))
{
let dist_sqrd = pos.0.distance_squared(tgt_pos.0);
// Follow, or return to idle
if dist_sqrd > AVG_FOLLOW_DIST.powf(2.0) {
if let Some(bearing) =
chaser.chase(&*terrain, pos.0, tgt_pos.0, AVG_FOLLOW_DIST)
{
inputs.move_dir = Vec2::from(bearing)
.try_normalized()
.unwrap_or(Vec2::zero());
inputs.jump.set_state(bearing.z > 1.0);
}
} else {
do_idle = true;
}
} else {
do_idle = true;
}
}
Activity::Attack(target, chaser, _) => {
if let (Some(tgt_pos), _tgt_stats, tgt_alignment) = (
positions.get(*target),
stats.get(*target),
alignments.get(*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) {
do_idle = true;
break 'activity;
}
}
let dist_sqrd = pos.0.distance_squared(tgt_pos.0);
if dist_sqrd < MIN_ATTACK_DIST.powf(2.0) {
// Close-range attack
inputs.look_dir = tgt_pos.0 - pos.0;
inputs.move_dir = Vec2::from(tgt_pos.0 - pos.0)
.try_normalized()
.unwrap_or(Vec2::unit_y())
* 0.01;
inputs.primary.set_state(true);
} else if dist_sqrd < MAX_CHASE_DIST.powf(2.0) {
// Long-range chase
if let Some(bearing) =
chaser.chase(&*terrain, pos.0, tgt_pos.0, 1.25)
{
inputs.move_dir = Vec2::from(bearing)
.try_normalized()
.unwrap_or(Vec2::zero());
inputs.jump.set_state(bearing.z > 1.0);
}
} else {
do_idle = true;
}
} else {
do_idle = true;
}
}
}
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;
if do_idle {
agent.activity = Activity::Idle(Vec2::zero());
}
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;
// Choose a new target to attack: only go out of our way to attack targets we are
// hostile toward!
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())
.join()
.filter(|(e, e_pos, e_stats, e_alignment)| {
(e_pos.0 - pos.0).magnitude_squared() < SIGHT_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::<Vec<_>>();
if rand::random::<f32>() < 0.02 {
inputs.roll.set_state(true);
}
if let Some(target) = (&entities).choose(&mut thread_rng()).cloned() {
agent.activity = Activity::Attack(target, Chaser::default(), time.0);
}
}
if target_character.movement == Glide && target_pos.0.z > pos.0.z + 5.0
// --- Activity overrides (in reverse order of priority: most important goes last!) ---
// Attack a target that's attacking us
if let Some(stats) = stats.get(entity) {
// Only if the attack was recent
if stats.health.last_change.0 < 5.0 {
if let comp::HealthSource::Attack { by } = stats.health.last_change.1.cause {
if !agent.activity.is_attack() {
if let Some(attacker) = uid_allocator.retrieve_entity_internal(by.id())
{
inputs.glide.set_state(true);
inputs.jump.set_state(true);
agent.activity =
Activity::Attack(attacker, Chaser::default(), time.0);
}
} 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()
};
choose_new = true;
// Follow owner if we're too far, or if they're under attack
if let Some(owner) = agent.owner {
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() {
agent.activity = Activity::Follow(owner, Chaser::default());
}
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();
// Attack owner's attacker
if let Some(owner_stats) = stats.get(owner) {
if owner_stats.health.last_change.0 < 5.0 {
if let comp::HealthSource::Attack { by } =
owner_stats.health.last_change.1.cause
{
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);
}
}
}
}
}
}
}

View File

@ -171,14 +171,16 @@ pub struct CachedVolGrid2d<'a, V: RectRasterableVol> {
// reference to the `VolGrid2d`
cache: Option<(Vec2<i32>, Arc<V>)>,
}
impl<'a, V: RectRasterableVol> CachedVolGrid2d<'a, V> {
pub fn new(vol_grid_2d: &'a VolGrid2d<V>) -> Self {
fn new(vol_grid_2d: &'a VolGrid2d<V>) -> Self {
Self {
vol_grid_2d,
cache: None,
}
}
}
impl<'a, V: RectRasterableVol + ReadVol> CachedVolGrid2d<'a, V> {
#[inline(always)]
pub fn get(&mut self, pos: Vec3<i32>) -> Result<&V::Vox, VolGrid2dError<V>> {

View File

@ -4,8 +4,12 @@ version = "0.4.0"
authors = ["Joshua Barretto <joshua.s.barretto@gmail.com>"]
edition = "2018"
[features]
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

@ -4,6 +4,10 @@ version = "0.4.0"
authors = ["Joshua Barretto <joshua.s.barretto@gmail.com>"]
edition = "2018"
[features]
worldgen = []
default = ["worldgen"]
[dependencies]
common = { package = "veloren-common", path = "../common" }
world = { package = "veloren-world", path = "../world" }
@ -19,7 +23,7 @@ scan_fmt = "0.2.4"
ron = "0.5.1"
serde = "1.0.102"
serde_derive = "1.0.102"
rand = "0.7.2"
rand = { version = "0.7.2", features = ["small_rng"] }
chrono = "0.4.9"
hashbrown = { version = "0.6.2", features = ["rayon", "serde", "nightly"] }
crossbeam = "=0.7.2"

View File

@ -1,4 +1,6 @@
use common::terrain::TerrainChunk;
#[cfg(not(feature = "worldgen"))]
use crate::test_world::World;
use common::{generation::ChunkSupplement, terrain::TerrainChunk};
use crossbeam::channel;
use hashbrown::{hash_map::Entry, HashMap};
use specs::Entity as EcsEntity;
@ -7,7 +9,8 @@ use std::sync::{
Arc,
};
use vek::*;
use world::{ChunkSupplement, World};
#[cfg(feature = "worldgen")]
use world::World;
type ChunkGenResult = (
Vec2<i32>,

View File

@ -7,13 +7,11 @@ use chrono::{NaiveTime, Timelike};
use common::{
assets, comp,
event::{EventBus, ServerEvent},
hierarchical::ChunkPath,
msg::{PlayerListUpdate, ServerMsg},
npc::{get_npc_name, NpcKind},
pathfinding::WorldPath,
state::TimeOfDay,
sync::{Uid, WorldSyncExt},
terrain::{Block, BlockKind, TerrainChunkSize},
terrain::TerrainChunkSize,
vol::RectVolSize,
};
use rand::Rng;
@ -249,13 +247,6 @@ lazy_static! {
true,
handle_debug,
),
ChatCommand::new(
"pathfind",
"{} {d} {d} {d}",
"/pathfind : Send a given entity with ID to the coordinates provided",
true,
handle_pathfind,
),
];
}
fn handle_give(server: &mut Server, entity: EcsEntity, args: String, _action: &ChatCommand) {
@ -467,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(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)
@ -476,6 +467,12 @@ fn handle_spawn(server: &mut Server, entity: EcsEntity, args: String, action: &C
match server.state.read_component_cloned::<comp::Pos>(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)
};
for _ in 0..amount {
let vel = Vec3::new(
rand::thread_rng().gen_range(-2.0, 3.0),
@ -495,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) {
@ -524,45 +522,6 @@ fn handle_spawn(server: &mut Server, entity: EcsEntity, args: String, action: &C
}
}
fn handle_pathfind(server: &mut Server, player: EcsEntity, args: String, action: &ChatCommand) {
if let (Some(id), Some(x), Some(y), Some(z)) =
scan_fmt_some!(&args, action.arg_fmt, u64, f32, f32, f32)
{
let entity = server.state.ecs().entity_from_uid(id);
if let Some(target_entity) = entity {
if let Some(start_pos) = server
.state
.read_component_cloned::<comp::Pos>(target_entity)
{
let target = start_pos.0 + Vec3::new(x, y, z);
let new_path = ChunkPath::new(&*server.state.terrain(), start_pos.0, target)
.get_worldpath(&*server.state.terrain());
server.state.write_component(
target_entity,
comp::Agent::Traveler {
path: new_path.clone(),
},
);
if let Some(path_positions) = &new_path.path {
for pos in path_positions {
server
.state
.set_block(*pos, Block::new(BlockKind::Normal, Rgb::new(255, 255, 0)));
}
}
}
} else {
server.notify_client(
player,
ServerMsg::private(format!("Unable to find entity with ID: {:?}", id)),
);
}
}
}
fn handle_players(server: &mut Server, entity: EcsEntity, _args: String, _action: &ChatCommand) {
let ecs = server.state.ecs();
let players = ecs.read_storage::<comp::Player>();
@ -624,17 +583,11 @@ fn handle_help(server: &mut Server, entity: EcsEntity, _args: String, _action: &
}
}
fn alignment_to_agent(alignment: &str, target: EcsEntity) -> Option<comp::Agent> {
fn parse_alignment(alignment: &str) -> Option<comp::Alignment> {
match alignment {
"hostile" => Some(comp::Agent::enemy()),
"friendly" => Some(comp::Agent::Pet {
target,
offset: Vec2::zero(),
}),
"traveler" => Some(comp::Agent::Traveler {
path: WorldPath::default(),
}),
// passive?
"wild" => Some(comp::Alignment::Wild),
"enemy" => Some(comp::Alignment::Enemy),
"npc" => Some(comp::Alignment::Npc),
_ => None,
}
}
@ -704,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,
@ -981,6 +935,20 @@ fn handle_tell(server: &mut Server, entity: EcsEntity, args: String, action: &Ch
}
}
#[cfg(not(feature = "worldgen"))]
fn handle_debug_column(
server: &mut Server,
entity: EcsEntity,
_args: String,
_action: &ChatCommand,
) {
server.notify_client(
entity,
ServerMsg::private(String::from("Unsupported without worldgen enabled")),
);
}
#[cfg(feature = "worldgen")]
fn handle_debug_column(server: &mut Server, entity: EcsEntity, args: String, action: &ChatCommand) {
let sim = server.world.sim();
let sampler = server.world.sample_columns();
@ -1120,10 +1088,11 @@ fn handle_remove_lights(
match opt_player_pos {
Some(player_pos) => {
let ecs = server.state.ecs();
for (entity, pos, _, _) in (
for (entity, pos, _, _, _) in (
&ecs.entities(),
&ecs.read_storage::<comp::Pos>(),
&ecs.read_storage::<comp::LightEmitter>(),
!&ecs.read_storage::<comp::WaypointArea>(),
!&ecs.read_storage::<comp::Player>(),
)
.join()

View File

@ -10,6 +10,8 @@ pub mod input;
pub mod metrics;
pub mod settings;
pub mod sys;
#[cfg(not(feature = "worldgen"))]
mod test_world;
// Reexports
pub use crate::{error::Error, input::Input, settings::ServerSettings};
@ -44,12 +46,16 @@ use std::{
sync::Arc,
time::{Duration, Instant},
};
#[cfg(not(feature = "worldgen"))]
use test_world::{World, WORLD_SIZE};
use uvth::{ThreadPool, ThreadPoolBuilder};
use vek::*;
#[cfg(feature = "worldgen")]
use world::{
sim::{FileOpts, WorldOpts, DEFAULT_WORLD_MAP, WORLD_SIZE},
World,
};
const CLIENT_TIMEOUT: f64 = 20.0; // Seconds
pub enum Event {
@ -108,6 +114,7 @@ impl Server {
state.ecs_mut().register::<RegionSubscription>();
state.ecs_mut().register::<Client>();
#[cfg(feature = "worldgen")]
let world = World::generate(
settings.world_seed,
WorldOpts {
@ -121,8 +128,15 @@ impl Server {
..WorldOpts::default()
},
);
#[cfg(feature = "worldgen")]
let map = world.sim().get_map();
#[cfg(not(feature = "worldgen"))]
let world = World::generate(settings.world_seed);
#[cfg(not(feature = "worldgen"))]
let map = vec![0];
#[cfg(feature = "worldgen")]
let spawn_point = {
// NOTE: all of these `.map(|e| e as [type])` calls should compile into no-ops,
// but are needed to be explicit about casting (and to make the compiler stop complaining)
@ -167,6 +181,9 @@ impl Server {
Vec3::new(spawn_location.x, spawn_location.y, z).map(|e| (e as f32)) + 0.5
};
#[cfg(not(feature = "worldgen"))]
let spawn_point = Vec3::new(0.0, 0.0, 256.0);
// set the spawn point we calculated above
state.ecs_mut().insert(SpawnPoint(spawn_point));
@ -283,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.
@ -818,12 +836,25 @@ impl Server {
stats,
body,
agent,
alignment,
scale,
} => {
state
.create_npc(pos, stats, body)
.with(agent)
.with(scale)
.with(alignment)
.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();
}
@ -940,7 +971,7 @@ impl Server {
(
&self.state.ecs().entities(),
&self.state.ecs().read_storage::<comp::Pos>(),
&self.state.ecs().read_storage::<comp::Agent>(),
!&self.state.ecs().read_storage::<comp::Player>(),
)
.join()
.filter(|(_, pos, _)| terrain.get(pos.0.map(|e| e.floor() as i32)).is_err())
@ -1212,6 +1243,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())

View File

@ -4,6 +4,7 @@ pub mod sentinel;
pub mod subscription;
pub mod terrain;
pub mod terrain_sync;
pub mod waypoint;
use specs::DispatcherBuilder;
use std::{marker::PhantomData, time::Instant};
@ -21,6 +22,7 @@ const SENTINEL_SYS: &str = "sentinel_sys";
const SUBSCRIPTION_SYS: &str = "server_subscription_sys";
const TERRAIN_SYNC_SYS: &str = "server_terrain_sync_sys";
const TERRAIN_SYS: &str = "server_terrain_sys";
const WAYPOINT_SYS: &str = "waypoint_sys";
pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) {
// TODO: makes some of these dependent on systems in common like the phys system
@ -37,6 +39,7 @@ pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) {
);
dispatch_builder.add(terrain_sync::Sys, TERRAIN_SYS, &[]);
dispatch_builder.add(terrain::Sys, TERRAIN_SYNC_SYS, &[TERRAIN_SYS]);
dispatch_builder.add(waypoint::Sys, WAYPOINT_SYS, &[]);
}
/// Used to keep track of how much time each system takes

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,87 +96,102 @@ 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>,
comp::Alignment,
)] = &[
(|| {
(
"Traveler".into(),
comp::Body::Humanoid(comp::humanoid::Body::random()),
Some(assets::load_expect_cloned("common.items.weapons.staff_1")),
comp::Alignment::Enemy,
)
}) as _,
(|| {
(
"Wolf".into(),
comp::Body::QuadrupedMedium(comp::quadruped_medium::Body::random()),
None,
comp::Alignment::Enemy,
)
}) as _,
(|| {
(
"Duck".into(),
comp::Body::BirdMedium(comp::bird_medium::Body::random()),
None,
comp::Alignment::Wild,
)
}) as _,
(|| {
(
"Rat".into(),
comp::Body::Critter(comp::critter::Body::random()),
None,
comp::Alignment::Wild,
)
}) as _,
(|| {
(
"Pig".into(),
comp::Body::QuadrupedSmall(comp::quadruped_small::Body::random()),
None,
comp::Alignment::Wild,
)
}),
];
let (name, mut body, main, alignment) = 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,
agent: comp::Agent::default().with_patrol_origin(entity.pos),
scale: comp::Scale(scale),
})
}
server_emitter.emit(ServerEvent::CreateNpc {
pos: Pos(npc.pos),
stats,
body,
agent: comp::Agent::enemy(),
scale: comp::Scale(scale),
})
}
}

View File

@ -0,0 +1,29 @@
use common::comp::{Player, Pos, Waypoint, WaypointArea};
use specs::{Entities, Join, ReadStorage, System, WriteStorage};
/// This system updates player waypoints
/// TODO: Make this faster by only considering local waypoints
pub struct Sys;
impl<'a> System<'a> for Sys {
type SystemData = (
Entities<'a>,
ReadStorage<'a, Pos>,
ReadStorage<'a, Player>,
ReadStorage<'a, WaypointArea>,
WriteStorage<'a, Waypoint>,
);
fn run(
&mut self,
(entities, positions, players, waypoint_areas, mut waypoints): Self::SystemData,
) {
for (entity, player_pos, _) in (&entities, &positions, &players).join() {
for (waypoint_pos, waypoint_area) in (&positions, &waypoint_areas).join() {
if player_pos.0.distance_squared(waypoint_pos.0) < waypoint_area.radius().powf(2.0)
{
let _ = waypoints.insert(entity, Waypoint::new(player_pos.0));
}
}
}
}
}

52
server/src/test_world.rs Normal file
View File

@ -0,0 +1,52 @@
use common::{
generation::{ChunkSupplement, EntityInfo, EntityKind},
terrain::{Block, BlockKind, TerrainChunk, TerrainChunkMeta, TerrainChunkSize},
vol::{ReadVol, RectVolSize, Vox, WriteVol},
};
use rand::{prelude::*, rngs::SmallRng};
use std::time::Duration;
use vek::*;
pub const WORLD_SIZE: Vec2<usize> = Vec2 { x: 1, y: 1 };
pub struct World;
impl World {
pub fn generate(_seed: u32) -> Self {
Self
}
pub fn tick(&self, dt: Duration) {}
pub fn generate_chunk(
&self,
chunk_pos: Vec2<i32>,
_should_continue: impl FnMut() -> bool,
) -> Result<(TerrainChunk, ChunkSupplement), ()> {
let (x, y) = chunk_pos.map(|e| e.to_le_bytes()).into_tuple();
let mut rng = SmallRng::from_seed([
x[0], x[1], x[2], x[3], y[0], y[1], y[2], y[3], x[0], x[1], x[2], x[3], y[0], y[1],
y[2], y[3],
]);
let height = rng.gen::<i32>() % 8;
let mut supplement = ChunkSupplement::default();
if chunk_pos.map(|e| e % 8 == 0).reduce_and() {
supplement = supplement.with_entity(EntityInfo {
pos: Vec3::<f32>::from(chunk_pos.map(|e| e as f32 * 32.0)) + Vec3::unit_z() * 256.0,
kind: EntityKind::Waypoint,
});
}
Ok((
TerrainChunk::new(
256 + if rng.gen::<u8>() < 64 { height } else { 0 },
Block::new(BlockKind::Dense, Rgb::new(200, 220, 255)),
Block::empty(),
TerrainChunkMeta::void(),
),
supplement,
))
}
}

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

@ -112,7 +112,7 @@ impl FigureMgr {
for (entity, pos, ori, scale, body, character, last_character, stats) in (
&ecs.entities(),
&ecs.read_storage::<Pos>(),
&ecs.read_storage::<Ori>(),
ecs.read_storage::<Ori>().maybe(),
ecs.read_storage::<Scale>().maybe(),
&ecs.read_storage::<Body>(),
ecs.read_storage::<CharacterState>().maybe(),
@ -121,6 +121,8 @@ impl FigureMgr {
)
.join()
{
let ori = ori.copied().unwrap_or(Ori(Vec3::unit_y()));
// Don't process figures outside the vd
let vd_frac = Vec2::from(pos.0 - player_pos)
.map2(TerrainChunk::RECT_SIZE, |d: f32, sz| {
@ -1225,7 +1227,7 @@ impl FigureMgr {
for (entity, _, _, body, stats, _) in (
&ecs.entities(),
&ecs.read_storage::<Pos>(),
&ecs.read_storage::<Ori>(),
ecs.read_storage::<Ori>().maybe(),
&ecs.read_storage::<Body>(),
ecs.read_storage::<Stats>().maybe(),
ecs.read_storage::<Scale>().maybe(),

View File

@ -1259,8 +1259,8 @@ impl<V: RectRasterableVol> Terrain<V> {
.fold(i32::MIN, |max, (_, chunk)| chunk.get_max_z().max(max));
let aabb = Aabb {
min: Vec3::from(aabr.min) + Vec3::unit_z() * (min_z - 1),
max: Vec3::from(aabr.max) + Vec3::unit_z() * (max_z + 1),
min: Vec3::from(aabr.min) + Vec3::unit_z() * (min_z - 2),
max: Vec3::from(aabr.max) + Vec3::unit_z() * (max_z + 2),
};
// Clone various things so that they can be moved into the thread.

View File

@ -19,6 +19,7 @@ use crate::{
util::Sampler,
};
use common::{
generation::{ChunkSupplement, EntityInfo, EntityKind},
terrain::{Block, BlockKind, TerrainChunk, TerrainChunkMeta, TerrainChunkSize},
vol::{ReadVol, RectVolSize, Vox, WriteVol},
};
@ -148,35 +149,31 @@ 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
let mut supplement = ChunkSupplement {
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()
},
};
if sim_chunk.contains_waypoint {
supplement = supplement.with_entity(EntityInfo {
pos: gen_entity_pos(),
kind: EntityKind::Waypoint,
});
}
Ok((chunk, supplement))
}
}
pub struct NpcInfo {
pub pos: Vec3<f32>,
pub boss: bool,
}
pub struct ChunkSupplement {
pub npcs: Vec<NpcInfo>,
}
impl Default for ChunkSupplement {
fn default() -> Self {
Self { npcs: Vec::new() }
}
}

View File

@ -1446,6 +1446,42 @@ impl WorldSim {
chunk.structures.town = maybe_town;
});
// Create waypoints
const WAYPOINT_EVERY: usize = 16;
let this = &self;
let waypoints = (0..WORLD_SIZE.x)
.step_by(WAYPOINT_EVERY)
.map(|i| {
(0..WORLD_SIZE.y)
.step_by(WAYPOINT_EVERY)
.map(move |j| (i, j))
})
.flatten()
.collect::<Vec<_>>()
.into_par_iter()
.filter_map(|(i, j)| {
let mut pos = Vec2::new(i as i32, j as i32);
// Slide the waypoints down hills
const MAX_ITERS: usize = 64;
for _ in 0..MAX_ITERS {
match this.get(pos)?.downhill {
Some(downhill) => {
pos = downhill
.map2(Vec2::from(TerrainChunkSize::RECT_SIZE), |e, sz: u32| {
e / (sz as i32)
})
}
None => return Some(pos),
}
}
Some(pos)
})
.collect::<Vec<_>>();
for waypoint in waypoints {
self.get_mut(waypoint).map(|sc| sc.contains_waypoint = true);
}
self.rng = rng;
self.locations = locations;
}
@ -1679,6 +1715,7 @@ pub struct SimChunk {
pub is_underwater: bool,
pub structures: Structures,
pub contains_waypoint: bool,
}
#[derive(Copy, Clone)]
@ -1922,6 +1959,7 @@ impl SimChunk {
location: None,
river,
structures: Structures { town: None },
contains_waypoint: false,
}
}