mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Merge branch 'zesterer/waystones' into 'master'
Pathfinding, better AI, waypoints See merge request veloren/veloren!753
This commit is contained in:
commit
6b4a09af30
@ -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
10
Cargo.lock
generated
@ -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"
|
||||
|
@ -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!,
|
||||
|
@ -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
BIN
assets/voxygen/voxel/object/campfire_lit.vox
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -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, ¤t));
|
||||
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(¤t);
|
||||
for neighbor in current_neighbors {
|
||||
let current_cheapest_score = cheapest_scores.get(¤t).unwrap_or(&f32::MAX);
|
||||
let neighbor_cheapest_score = cheapest_scores.get(&neighbor).unwrap_or(&f32::MAX);
|
||||
let score = current_cheapest_score + transition_cost(¤t, &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()
|
||||
}
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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
24
common/src/generation.rs
Normal 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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
268
common/src/path.rs
Normal 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(),
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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>();
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>> {
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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>,
|
||||
|
@ -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()
|
||||
|
@ -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())
|
||||
|
@ -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
|
||||
|
@ -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),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
29
server/src/sys/waypoint.rs
Normal file
29
server/src/sys/waypoint.rs
Normal 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
52
server/src/test_world.rs
Normal 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,
|
||||
))
|
||||
}
|
||||
}
|
@ -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)),
|
||||
|
@ -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(),
|
||||
|
@ -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.
|
||||
|
@ -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() }
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user