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 fullscreen and window size to settings so that they can be persisted
|
||||||
- Added coverage based scaling for pixel art
|
- Added coverage based scaling for pixel art
|
||||||
- 28 new mobs
|
- 28 new mobs
|
||||||
|
- Added waypoints
|
||||||
|
- Added pathfinding to NPCs
|
||||||
|
- Overhauled NPC AI
|
||||||
|
- Pets now attack enemies and defend their owners
|
||||||
|
|
||||||
|
|
||||||
### Changed
|
### 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_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_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_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]]
|
[[package]]
|
||||||
@ -2435,6 +2436,14 @@ dependencies = [
|
|||||||
"rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
"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]]
|
[[package]]
|
||||||
name = "rand_xorshift"
|
name = "rand_xorshift"
|
||||||
version = "0.1.1"
|
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.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_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.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_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 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"
|
"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",
|
"hud.press_key_to_toggle_debug_info_fmt": "Press {key} to toogle debug info",
|
||||||
|
|
||||||
// Respawn message
|
// Respawn message
|
||||||
"hud.press_key_to_respawn": r#"Press {key} to respawn at your Waypoint.
|
"hud.press_key_to_respawn": r#"Press {key} to respawn at the last campfire you visited."#,
|
||||||
|
|
||||||
Press Enter, type in /waypoint and confirm to set it here."#,
|
|
||||||
|
|
||||||
// Welcome message
|
// Welcome message
|
||||||
"hud.welcome": r#"Welcome to the Veloren Alpha!,
|
"hud.welcome": r#"Welcome to the Veloren Alpha!,
|
||||||
|
@ -66,7 +66,7 @@ float shadow_at(vec3 wpos, vec3 wnorm) {
|
|||||||
|
|
||||||
vec3 diff = shadow_pos - wpos;
|
vec3 diff = shadow_pos - wpos;
|
||||||
if (diff.z >= 0.0) {
|
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);
|
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 core::cmp::Ordering::Equal;
|
||||||
use hashbrown::{HashMap, HashSet};
|
use hashbrown::{HashMap, HashSet};
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
@ -5,7 +6,7 @@ use std::collections::BinaryHeap;
|
|||||||
use std::f32;
|
use std::f32;
|
||||||
use std::hash::Hash;
|
use std::hash::Hash;
|
||||||
|
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
pub struct PathEntry<S> {
|
pub struct PathEntry<S> {
|
||||||
cost: f32,
|
cost: f32,
|
||||||
node: S,
|
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>
|
pub enum PathResult<T> {
|
||||||
where
|
None(Path<T>),
|
||||||
S: Clone + Eq + Hash,
|
Exhausted(Path<T>),
|
||||||
{
|
Path(Path<T>),
|
||||||
let mut path = Vec::new();
|
Pending,
|
||||||
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 fn astar<S, I>(
|
#[derive(Clone, Debug)]
|
||||||
initial: S,
|
pub struct Astar<S: Clone + Eq + Hash> {
|
||||||
target: S,
|
iter: usize,
|
||||||
mut heuristic: impl FnMut(&S, &S) -> f32,
|
max_iters: usize,
|
||||||
mut neighbors: impl FnMut(&S) -> I,
|
potential_nodes: BinaryHeap<PathEntry<S>>,
|
||||||
mut transition_cost: impl FnMut(&S, &S) -> f32,
|
came_from: HashMap<S, S>,
|
||||||
) -> Option<Vec<S>>
|
cheapest_scores: HashMap<S, f32>,
|
||||||
where
|
final_scores: HashMap<S, f32>,
|
||||||
S: Clone + Eq + Hash,
|
visited: HashSet<S>,
|
||||||
I: IntoIterator<Item = S>,
|
lowest_cost: Option<S>,
|
||||||
{
|
}
|
||||||
// Set of discovered nodes so far
|
|
||||||
let mut potential_nodes = BinaryHeap::new();
|
|
||||||
potential_nodes.push(PathEntry {
|
|
||||||
cost: 0.0f32,
|
|
||||||
node: initial.clone(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// For entry e, contains the cheapest node preceding it on the known path from start to e
|
impl<S: Clone + Eq + Hash> Astar<S> {
|
||||||
let mut came_from = HashMap::new();
|
pub fn new(max_iters: usize, start: S, heuristic: impl FnOnce(&S) -> f32) -> Self {
|
||||||
|
Self {
|
||||||
// Contains cheapest cost from 'initial' to the current entry
|
max_iters,
|
||||||
let mut cheapest_scores = HashMap::new();
|
iter: 0,
|
||||||
cheapest_scores.insert(initial.clone(), 0.0f32);
|
potential_nodes: std::iter::once(PathEntry {
|
||||||
|
cost: 0.0,
|
||||||
// Contains cheapest score to get to node + heuristic to the end, for an entry
|
node: start.clone(),
|
||||||
let mut final_scores = HashMap::new();
|
})
|
||||||
final_scores.insert(initial.clone(), heuristic(&initial, &target));
|
.collect(),
|
||||||
|
came_from: HashMap::default(),
|
||||||
// Set of nodes we have already visited
|
cheapest_scores: std::iter::once((start.clone(), 0.0)).collect(),
|
||||||
let mut visited = HashSet::new();
|
final_scores: std::iter::once((start.clone(), heuristic(&start))).collect(),
|
||||||
visited.insert(initial.clone());
|
visited: std::iter::once(start).collect(),
|
||||||
|
lowest_cost: None,
|
||||||
while let Some(PathEntry { node: current, .. }) = potential_nodes.pop() {
|
|
||||||
if current == target {
|
|
||||||
return Some(reconstruct_path(&came_from, ¤t));
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let current_neighbors = neighbors(¤t);
|
pub fn poll<I>(
|
||||||
for neighbor in current_neighbors {
|
&mut self,
|
||||||
let current_cheapest_score = cheapest_scores.get(¤t).unwrap_or(&f32::MAX);
|
iters: usize,
|
||||||
let neighbor_cheapest_score = cheapest_scores.get(&neighbor).unwrap_or(&f32::MAX);
|
mut heuristic: impl FnMut(&S) -> f32,
|
||||||
let score = current_cheapest_score + transition_cost(¤t, &neighbor);
|
mut neighbors: impl FnMut(&S) -> I,
|
||||||
if score < *neighbor_cheapest_score {
|
mut transition: impl FnMut(&S, &S) -> f32,
|
||||||
// Path to the neighbor is better than anything yet recorded
|
mut satisfied: impl FnMut(&S) -> bool,
|
||||||
came_from.insert(neighbor.to_owned(), current.to_owned());
|
) -> PathResult<S>
|
||||||
cheapest_scores.insert(neighbor.clone(), score);
|
where
|
||||||
let neighbor_score = score + heuristic(&neighbor, &target);
|
I: Iterator<Item = S>,
|
||||||
final_scores.insert(neighbor.clone(), neighbor_score);
|
{
|
||||||
|
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()) {
|
let cost = node_cheapest + transition(&node, &neighbor);
|
||||||
potential_nodes.push(PathEntry {
|
if cost < *neighbor_cheapest {
|
||||||
node: neighbor.clone(),
|
self.came_from.insert(neighbor.clone(), node.clone());
|
||||||
cost: neighbor_score,
|
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::{Component, Entity as EcsEntity};
|
||||||
use specs_idvs::IDVStorage;
|
use specs_idvs::IDVStorage;
|
||||||
use vek::*;
|
use vek::*;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||||
pub enum Agent {
|
pub enum Alignment {
|
||||||
Wanderer(Vec2<f32>),
|
Wild,
|
||||||
Pet {
|
Enemy,
|
||||||
target: EcsEntity,
|
Npc,
|
||||||
offset: Vec2<f32>,
|
}
|
||||||
},
|
|
||||||
Enemy {
|
impl Alignment {
|
||||||
bearing: Vec2<f32>,
|
pub fn hostile_towards(self, other: Alignment) -> bool {
|
||||||
target: Option<EcsEntity>,
|
match (self, other) {
|
||||||
},
|
(Alignment::Wild, Alignment::Npc) => false,
|
||||||
Traveler {
|
_ => self != other,
|
||||||
path: WorldPath,
|
}
|
||||||
},
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
impl Agent {
|
||||||
pub fn enemy() -> Self {
|
pub fn with_pet(mut self, owner: EcsEntity) -> Self {
|
||||||
Agent::Enemy {
|
self.owner = Some(owner);
|
||||||
bearing: Vec2::zero(),
|
self
|
||||||
target: None,
|
}
|
||||||
}
|
|
||||||
|
pub fn with_patrol_origin(mut self, origin: Vec3<f32>) -> Self {
|
||||||
|
self.patrol_origin = Some(origin);
|
||||||
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component for Agent {
|
impl Component for Agent {
|
||||||
type Storage = IDVStorage<Self>;
|
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,
|
CraftingBench = 48,
|
||||||
BoltFire = 49,
|
BoltFire = 49,
|
||||||
ArrowSnake = 50,
|
ArrowSnake = 50,
|
||||||
|
CampfireLit = 51,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Body {
|
impl Body {
|
||||||
@ -63,7 +64,7 @@ impl Body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ALL_OBJECTS: [Body; 50] = [
|
const ALL_OBJECTS: [Body; 51] = [
|
||||||
Body::Arrow,
|
Body::Arrow,
|
||||||
Body::Bomb,
|
Body::Bomb,
|
||||||
Body::Scarecrow,
|
Body::Scarecrow,
|
||||||
@ -82,6 +83,7 @@ const ALL_OBJECTS: [Body; 50] = [
|
|||||||
Body::Pumpkin4,
|
Body::Pumpkin4,
|
||||||
Body::Pumpkin5,
|
Body::Pumpkin5,
|
||||||
Body::Campfire,
|
Body::Campfire,
|
||||||
|
Body::CampfireLit,
|
||||||
Body::LanternGround,
|
Body::LanternGround,
|
||||||
Body::LanternGroundOpen,
|
Body::LanternGroundOpen,
|
||||||
Body::LanternStanding,
|
Body::LanternStanding,
|
||||||
|
@ -20,3 +20,22 @@ impl Waypoint {
|
|||||||
impl Component for Waypoint {
|
impl Component for Waypoint {
|
||||||
type Storage = FlaggedStorage<Self, IDVStorage<Self>>;
|
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 admin;
|
||||||
mod agent;
|
pub mod agent;
|
||||||
mod body;
|
mod body;
|
||||||
mod character_state;
|
mod character_state;
|
||||||
mod controller;
|
mod controller;
|
||||||
@ -16,7 +16,7 @@ mod visual;
|
|||||||
|
|
||||||
// Reexports
|
// Reexports
|
||||||
pub use admin::Admin;
|
pub use admin::Admin;
|
||||||
pub use agent::Agent;
|
pub use agent::{Agent, Alignment};
|
||||||
pub use body::{
|
pub use body::{
|
||||||
biped_large, bird_medium, bird_small, critter, dragon, fish_medium, fish_small, humanoid,
|
biped_large, bird_medium, bird_small, critter, dragon, fish_medium, fish_small, humanoid,
|
||||||
object, quadruped_medium, quadruped_small, Body,
|
object, quadruped_medium, quadruped_small, Body,
|
||||||
@ -30,7 +30,7 @@ pub use energy::{Energy, EnergySource};
|
|||||||
pub use inputs::CanBuild;
|
pub use inputs::CanBuild;
|
||||||
pub use inventory::{item, Inventory, InventoryUpdate, Item, ItemKind};
|
pub use inventory::{item, Inventory, InventoryUpdate, Item, ItemKind};
|
||||||
pub use last::Last;
|
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 phys::{ForceUpdate, Gravity, Mass, Ori, PhysicsState, Pos, Scale, Sticky, Vel};
|
||||||
pub use player::Player;
|
pub use player::Player;
|
||||||
pub use projectile::Projectile;
|
pub use projectile::Projectile;
|
||||||
|
@ -102,8 +102,10 @@ pub enum ServerEvent {
|
|||||||
stats: comp::Stats,
|
stats: comp::Stats,
|
||||||
body: comp::Body,
|
body: comp::Body,
|
||||||
agent: comp::Agent,
|
agent: comp::Agent,
|
||||||
|
alignment: comp::Alignment,
|
||||||
scale: comp::Scale,
|
scale: comp::Scale,
|
||||||
},
|
},
|
||||||
|
CreateWaypoint(Vec3<f32>),
|
||||||
ClientDisconnect(EcsEntity),
|
ClientDisconnect(EcsEntity),
|
||||||
ChunkRequest(EcsEntity, Vec2<i32>),
|
ChunkRequest(EcsEntity, Vec2<i32>),
|
||||||
ChatCmd(EcsEntity, String),
|
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>(
|
pub fn chunk_get_neighbors<V: RectRasterableVol + ReadVol + Debug>(
|
||||||
_vol: &VolGrid2d<V>,
|
_vol: &VolGrid2d<V>,
|
||||||
pos: &Vec2<i32>,
|
pos: &Vec2<i32>,
|
||||||
) -> impl IntoIterator<Item = Vec2<i32>> {
|
) -> impl Iterator<Item = Vec2<i32>> {
|
||||||
let directions = vec![
|
let directions = vec![
|
||||||
Vec2::new(1, 0), // Right chunk
|
Vec2::new(1, 0), // Right chunk
|
||||||
Vec2::new(-1, 0), // Left chunk
|
Vec2::new(-1, 0), // Left chunk
|
||||||
@ -53,7 +53,14 @@ impl ChunkPath {
|
|||||||
Vec2::new(0, -1), // Bottom chunk
|
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()
|
neighbors.into_iter()
|
||||||
}
|
}
|
||||||
@ -61,7 +68,7 @@ impl ChunkPath {
|
|||||||
&mut self,
|
&mut self,
|
||||||
vol: &VolGrid2d<V>,
|
vol: &VolGrid2d<V>,
|
||||||
pos: Vec3<i32>,
|
pos: Vec3<i32>,
|
||||||
) -> impl IntoIterator<Item = Vec3<i32>> {
|
) -> impl Iterator<Item = Vec3<i32>> {
|
||||||
let directions = vec![
|
let directions = vec![
|
||||||
Vec3::new(0, 1, 0), // Forward
|
Vec3::new(0, 1, 0), // Forward
|
||||||
Vec3::new(0, 1, 1), // Forward upward
|
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));
|
.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;
|
return is_walkable_position && is_within_chunk;
|
||||||
@ -110,11 +117,11 @@ impl ChunkPath {
|
|||||||
pub fn get_worldpath<V: RectRasterableVol + ReadVol + Debug>(
|
pub fn get_worldpath<V: RectRasterableVol + ReadVol + Debug>(
|
||||||
&mut self,
|
&mut self,
|
||||||
vol: &VolGrid2d<V>,
|
vol: &VolGrid2d<V>,
|
||||||
) -> WorldPath {
|
) -> Result<WorldPath, ()> {
|
||||||
let wp = WorldPath::new(vol, self.from, self.dest, |vol, pos| {
|
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
|
wp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
#![deny(unsafe_code)]
|
#![deny(unsafe_code)]
|
||||||
#![type_length_limit = "1664759"]
|
#![type_length_limit = "1664759"]
|
||||||
#![feature(trait_alias, arbitrary_enum_discriminant)]
|
#![feature(trait_alias, arbitrary_enum_discriminant, label_break_value)]
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate serde_derive;
|
extern crate serde_derive;
|
||||||
@ -14,10 +14,10 @@ pub mod comp;
|
|||||||
pub mod effect;
|
pub mod effect;
|
||||||
pub mod event;
|
pub mod event;
|
||||||
pub mod figure;
|
pub mod figure;
|
||||||
pub mod hierarchical;
|
pub mod generation;
|
||||||
pub mod msg;
|
pub mod msg;
|
||||||
pub mod npc;
|
pub mod npc;
|
||||||
pub mod pathfinding;
|
pub mod path;
|
||||||
pub mod ray;
|
pub mod ray;
|
||||||
pub mod region;
|
pub mod region;
|
||||||
pub mod state;
|
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::Ori>>();
|
||||||
ecs.register::<comp::Last<comp::CharacterState>>();
|
ecs.register::<comp::Last<comp::CharacterState>>();
|
||||||
ecs.register::<comp::Agent>();
|
ecs.register::<comp::Agent>();
|
||||||
|
ecs.register::<comp::Alignment>();
|
||||||
|
ecs.register::<comp::WaypointArea>();
|
||||||
ecs.register::<comp::ForceUpdate>();
|
ecs.register::<comp::ForceUpdate>();
|
||||||
ecs.register::<comp::InventoryUpdate>();
|
ecs.register::<comp::InventoryUpdate>();
|
||||||
ecs.register::<comp::Admin>();
|
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 crate::terrain::TerrainGrid;
|
||||||
use rand::{seq::SliceRandom, thread_rng};
|
use crate::{
|
||||||
use specs::{Entities, Join, ReadExpect, ReadStorage, System, WriteStorage};
|
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::*;
|
use vek::*;
|
||||||
|
|
||||||
/// This system will allow NPCs to modify their controller
|
/// This system will allow NPCs to modify their controller
|
||||||
pub struct Sys;
|
pub struct Sys;
|
||||||
impl<'a> System<'a> for Sys {
|
impl<'a> System<'a> for Sys {
|
||||||
type SystemData = (
|
type SystemData = (
|
||||||
|
Read<'a, UidAllocator>,
|
||||||
|
Read<'a, Time>,
|
||||||
Entities<'a>,
|
Entities<'a>,
|
||||||
ReadStorage<'a, Pos>,
|
ReadStorage<'a, Pos>,
|
||||||
ReadStorage<'a, Stats>,
|
ReadStorage<'a, Stats>,
|
||||||
ReadStorage<'a, CharacterState>,
|
|
||||||
ReadExpect<'a, TerrainGrid>,
|
ReadExpect<'a, TerrainGrid>,
|
||||||
|
ReadStorage<'a, Alignment>,
|
||||||
WriteStorage<'a, Agent>,
|
WriteStorage<'a, Agent>,
|
||||||
WriteStorage<'a, Controller>,
|
WriteStorage<'a, Controller>,
|
||||||
ReadStorage<'a, MountState>,
|
ReadStorage<'a, MountState>,
|
||||||
@ -25,19 +31,22 @@ impl<'a> System<'a> for Sys {
|
|||||||
fn run(
|
fn run(
|
||||||
&mut self,
|
&mut self,
|
||||||
(
|
(
|
||||||
|
uid_allocator,
|
||||||
|
time,
|
||||||
entities,
|
entities,
|
||||||
positions,
|
positions,
|
||||||
stats,
|
stats,
|
||||||
character_states,
|
|
||||||
terrain,
|
terrain,
|
||||||
|
alignments,
|
||||||
mut agents,
|
mut agents,
|
||||||
mut controllers,
|
mut controllers,
|
||||||
mount_states,
|
mount_states,
|
||||||
): Self::SystemData,
|
): Self::SystemData,
|
||||||
) {
|
) {
|
||||||
for (entity, pos, agent, controller, mount_state) in (
|
for (entity, pos, alignment, agent, controller, mount_state) in (
|
||||||
&entities,
|
&entities,
|
||||||
&positions,
|
&positions,
|
||||||
|
alignments.maybe(),
|
||||||
&mut agents,
|
&mut agents,
|
||||||
&mut controllers,
|
&mut controllers,
|
||||||
mount_states.maybe(),
|
mount_states.maybe(),
|
||||||
@ -62,149 +71,175 @@ impl<'a> System<'a> for Sys {
|
|||||||
|
|
||||||
let mut inputs = &mut controller.inputs;
|
let mut inputs = &mut controller.inputs;
|
||||||
|
|
||||||
match agent {
|
const AVG_FOLLOW_DIST: f32 = 6.0;
|
||||||
Agent::Traveler { path } => {
|
const MAX_FOLLOW_DIST: f32 = 12.0;
|
||||||
let mut new_path: Option<WorldPath> = None;
|
const MAX_CHASE_DIST: f32 = 24.0;
|
||||||
let is_destination = |cur_pos: Vec3<i32>, dest: Vec3<i32>| {
|
const SIGHT_DIST: f32 = 30.0;
|
||||||
Vec2::<i32>::from(cur_pos) == Vec2::<i32>::from(dest)
|
const MIN_ATTACK_DIST: f32 = 3.25;
|
||||||
};
|
|
||||||
|
|
||||||
let found_destination = || {
|
let mut do_idle = false;
|
||||||
const MAX_TRAVEL_DIST: f32 = 200.0;
|
let mut choose_target = false;
|
||||||
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),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
path.move_along_path(
|
'activity: {
|
||||||
&*terrain,
|
match &mut agent.activity {
|
||||||
pos,
|
Activity::Idle(bearing) => {
|
||||||
&mut inputs,
|
*bearing += Vec2::new(
|
||||||
is_destination,
|
thread_rng().gen::<f32>() - 0.5,
|
||||||
found_destination,
|
thread_rng().gen::<f32>() - 0.5,
|
||||||
);
|
) * 0.1
|
||||||
|
- *bearing * 0.01
|
||||||
if let Some(new_path) = new_path {
|
- if let Some(patrol_origin) = agent.patrol_origin {
|
||||||
*path = new_path;
|
Vec2::<f32>::from(pos.0 - patrol_origin) * 0.0002
|
||||||
}
|
|
||||||
}
|
|
||||||
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()
|
|
||||||
} else {
|
} else {
|
||||||
Vec2::zero()
|
Vec2::zero()
|
||||||
};
|
};
|
||||||
}
|
|
||||||
_ => inputs.move_dir = Vec2::zero(),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Change offset occasionally.
|
if bearing.magnitude_squared() > 0.25f32.powf(2.0) {
|
||||||
if rand::random::<f32>() < 0.003 {
|
inputs.move_dir = bearing.normalized() * 0.65;
|
||||||
*offset =
|
}
|
||||||
Vec2::new(rand::random::<f32>() - 0.5, rand::random::<f32>() - 0.5)
|
|
||||||
* 10.0;
|
// 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))) =
|
if do_idle {
|
||||||
target.map(|target| {
|
agent.activity = Activity::Idle(Vec2::zero());
|
||||||
(
|
}
|
||||||
positions.get(target),
|
|
||||||
stats.get(target),
|
|
||||||
character_states.get(target),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
{
|
|
||||||
inputs.look_dir = target_pos.0 - pos.0;
|
|
||||||
|
|
||||||
let dist = Vec2::<f32>::from(target_pos.0 - pos.0).magnitude();
|
// Choose a new target to attack: only go out of our way to attack targets we are
|
||||||
if target_stats.is_dead {
|
// hostile toward!
|
||||||
choose_new = true;
|
if choose_target {
|
||||||
} else if dist < 0.001 {
|
// Search for new targets (this looks expensive, but it's only run occasionally)
|
||||||
// Probably can only happen when entities are at a different z-level
|
// TODO: Replace this with a better system that doesn't consider *all* entities
|
||||||
// since at the same level repulsion would keep them apart.
|
let entities = (&entities, &positions, &stats, alignments.maybe())
|
||||||
// Distinct from the first if block since we may want to change the
|
.join()
|
||||||
// behavior for this case.
|
.filter(|(e, e_pos, e_stats, e_alignment)| {
|
||||||
choose_new = true;
|
(e_pos.0 - pos.0).magnitude_squared() < SIGHT_DIST.powf(2.0)
|
||||||
} else if dist < MIN_ATTACK_DIST {
|
&& *e != entity
|
||||||
// Fight (and slowly move closer)
|
&& !e_stats.is_dead
|
||||||
inputs.move_dir =
|
&& alignment
|
||||||
Vec2::<f32>::from(target_pos.0 - pos.0).normalized() * 0.01;
|
.and_then(|a| e_alignment.map(|b| a.hostile_towards(*b)))
|
||||||
inputs.primary.set_state(true);
|
.unwrap_or(false)
|
||||||
} else if dist < SIGHT_DIST {
|
})
|
||||||
inputs.move_dir =
|
.map(|(e, _, _, _)| e)
|
||||||
Vec2::<f32>::from(target_pos.0 - pos.0).normalized() * 0.96;
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
if rand::random::<f32>() < 0.02 {
|
if let Some(target) = (&entities).choose(&mut thread_rng()).cloned() {
|
||||||
inputs.roll.set_state(true);
|
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);
|
agent.activity =
|
||||||
inputs.jump.set_state(true);
|
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 {
|
// Follow owner if we're too far, or if they're under attack
|
||||||
bearing.normalized()
|
if let Some(owner) = agent.owner {
|
||||||
} else {
|
if let Some(owner_pos) = positions.get(owner) {
|
||||||
Vec2::zero()
|
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());
|
||||||
choose_new = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if choose_new && rand::random::<f32>() < 0.1 {
|
// Attack owner's attacker
|
||||||
let entities = (&entities, &positions, &stats)
|
if let Some(owner_stats) = stats.get(owner) {
|
||||||
.join()
|
if owner_stats.health.last_change.0 < 5.0 {
|
||||||
.filter(|(e, e_pos, e_stats)| {
|
if let comp::HealthSource::Attack { by } =
|
||||||
(e_pos.0 - pos.0).magnitude() < SIGHT_DIST
|
owner_stats.health.last_change.1.cause
|
||||||
&& *e != entity
|
{
|
||||||
&& !e_stats.is_dead
|
if !agent.activity.is_attack() {
|
||||||
})
|
if let Some(attacker) =
|
||||||
.map(|(e, _, _)| e)
|
uid_allocator.retrieve_entity_internal(by.id())
|
||||||
.collect::<Vec<_>>();
|
{
|
||||||
|
agent.activity =
|
||||||
let mut rng = thread_rng();
|
Activity::Attack(attacker, Chaser::default(), time.0);
|
||||||
*target = (&entities).choose(&mut rng).cloned();
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -171,14 +171,16 @@ pub struct CachedVolGrid2d<'a, V: RectRasterableVol> {
|
|||||||
// reference to the `VolGrid2d`
|
// reference to the `VolGrid2d`
|
||||||
cache: Option<(Vec2<i32>, Arc<V>)>,
|
cache: Option<(Vec2<i32>, Arc<V>)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, V: RectRasterableVol> CachedVolGrid2d<'a, 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 {
|
Self {
|
||||||
vol_grid_2d,
|
vol_grid_2d,
|
||||||
cache: None,
|
cache: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, V: RectRasterableVol + ReadVol> CachedVolGrid2d<'a, V> {
|
impl<'a, V: RectRasterableVol + ReadVol> CachedVolGrid2d<'a, V> {
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn get(&mut self, pos: Vec3<i32>) -> Result<&V::Vox, VolGrid2dError<V>> {
|
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>"]
|
authors = ["Joshua Barretto <joshua.s.barretto@gmail.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
worldgen = ["server/worldgen"]
|
||||||
|
default = ["worldgen"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
server = { package = "veloren-server", path = "../server" }
|
server = { package = "veloren-server", path = "../server", default-features = false }
|
||||||
common = { package = "veloren-common", path = "../common" }
|
common = { package = "veloren-common", path = "../common" }
|
||||||
|
|
||||||
log = "0.4.8"
|
log = "0.4.8"
|
||||||
|
@ -4,6 +4,10 @@ version = "0.4.0"
|
|||||||
authors = ["Joshua Barretto <joshua.s.barretto@gmail.com>"]
|
authors = ["Joshua Barretto <joshua.s.barretto@gmail.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
worldgen = []
|
||||||
|
default = ["worldgen"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
common = { package = "veloren-common", path = "../common" }
|
common = { package = "veloren-common", path = "../common" }
|
||||||
world = { package = "veloren-world", path = "../world" }
|
world = { package = "veloren-world", path = "../world" }
|
||||||
@ -19,7 +23,7 @@ scan_fmt = "0.2.4"
|
|||||||
ron = "0.5.1"
|
ron = "0.5.1"
|
||||||
serde = "1.0.102"
|
serde = "1.0.102"
|
||||||
serde_derive = "1.0.102"
|
serde_derive = "1.0.102"
|
||||||
rand = "0.7.2"
|
rand = { version = "0.7.2", features = ["small_rng"] }
|
||||||
chrono = "0.4.9"
|
chrono = "0.4.9"
|
||||||
hashbrown = { version = "0.6.2", features = ["rayon", "serde", "nightly"] }
|
hashbrown = { version = "0.6.2", features = ["rayon", "serde", "nightly"] }
|
||||||
crossbeam = "=0.7.2"
|
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 crossbeam::channel;
|
||||||
use hashbrown::{hash_map::Entry, HashMap};
|
use hashbrown::{hash_map::Entry, HashMap};
|
||||||
use specs::Entity as EcsEntity;
|
use specs::Entity as EcsEntity;
|
||||||
@ -7,7 +9,8 @@ use std::sync::{
|
|||||||
Arc,
|
Arc,
|
||||||
};
|
};
|
||||||
use vek::*;
|
use vek::*;
|
||||||
use world::{ChunkSupplement, World};
|
#[cfg(feature = "worldgen")]
|
||||||
|
use world::World;
|
||||||
|
|
||||||
type ChunkGenResult = (
|
type ChunkGenResult = (
|
||||||
Vec2<i32>,
|
Vec2<i32>,
|
||||||
|
@ -7,13 +7,11 @@ use chrono::{NaiveTime, Timelike};
|
|||||||
use common::{
|
use common::{
|
||||||
assets, comp,
|
assets, comp,
|
||||||
event::{EventBus, ServerEvent},
|
event::{EventBus, ServerEvent},
|
||||||
hierarchical::ChunkPath,
|
|
||||||
msg::{PlayerListUpdate, ServerMsg},
|
msg::{PlayerListUpdate, ServerMsg},
|
||||||
npc::{get_npc_name, NpcKind},
|
npc::{get_npc_name, NpcKind},
|
||||||
pathfinding::WorldPath,
|
|
||||||
state::TimeOfDay,
|
state::TimeOfDay,
|
||||||
sync::{Uid, WorldSyncExt},
|
sync::{Uid, WorldSyncExt},
|
||||||
terrain::{Block, BlockKind, TerrainChunkSize},
|
terrain::TerrainChunkSize,
|
||||||
vol::RectVolSize,
|
vol::RectVolSize,
|
||||||
};
|
};
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
@ -249,13 +247,6 @@ lazy_static! {
|
|||||||
true,
|
true,
|
||||||
handle_debug,
|
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) {
|
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) {
|
fn handle_spawn(server: &mut Server, entity: EcsEntity, args: String, action: &ChatCommand) {
|
||||||
match scan_fmt_some!(&args, action.arg_fmt, String, NpcKind, String) {
|
match scan_fmt_some!(&args, action.arg_fmt, String, NpcKind, String) {
|
||||||
(Some(opt_align), Some(id), opt_amount) => {
|
(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
|
let amount = opt_amount
|
||||||
.and_then(|a| a.parse().ok())
|
.and_then(|a| a.parse().ok())
|
||||||
.filter(|x| *x > 0)
|
.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) {
|
match server.state.read_component_cloned::<comp::Pos>(entity) {
|
||||||
Some(pos) => {
|
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 {
|
for _ in 0..amount {
|
||||||
let vel = Vec3::new(
|
let vel = Vec3::new(
|
||||||
rand::thread_rng().gen_range(-2.0, 3.0),
|
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::Vel(vel))
|
||||||
.with(comp::MountState::Unmounted)
|
.with(comp::MountState::Unmounted)
|
||||||
.with(agent.clone())
|
.with(agent.clone())
|
||||||
|
.with(alignment)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
if let Some(uid) = server.state.ecs().uid_from_entity(new_entity) {
|
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) {
|
fn handle_players(server: &mut Server, entity: EcsEntity, _args: String, _action: &ChatCommand) {
|
||||||
let ecs = server.state.ecs();
|
let ecs = server.state.ecs();
|
||||||
let players = ecs.read_storage::<comp::Player>();
|
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 {
|
match alignment {
|
||||||
"hostile" => Some(comp::Agent::enemy()),
|
"wild" => Some(comp::Alignment::Wild),
|
||||||
"friendly" => Some(comp::Agent::Pet {
|
"enemy" => Some(comp::Alignment::Enemy),
|
||||||
target,
|
"npc" => Some(comp::Alignment::Npc),
|
||||||
offset: Vec2::zero(),
|
|
||||||
}),
|
|
||||||
"traveler" => Some(comp::Agent::Traveler {
|
|
||||||
path: WorldPath::default(),
|
|
||||||
}),
|
|
||||||
// passive?
|
|
||||||
_ => None,
|
_ => 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_4") => comp::object::Body::Pumpkin4,
|
||||||
Ok("pumpkin_5") => comp::object::Body::Pumpkin5,
|
Ok("pumpkin_5") => comp::object::Body::Pumpkin5,
|
||||||
Ok("campfire") => comp::object::Body::Campfire,
|
Ok("campfire") => comp::object::Body::Campfire,
|
||||||
|
Ok("campfire_lit") => comp::object::Body::CampfireLit,
|
||||||
Ok("lantern_ground") => comp::object::Body::LanternGround,
|
Ok("lantern_ground") => comp::object::Body::LanternGround,
|
||||||
Ok("lantern_ground_open") => comp::object::Body::LanternGroundOpen,
|
Ok("lantern_ground_open") => comp::object::Body::LanternGroundOpen,
|
||||||
Ok("lantern_2") => comp::object::Body::LanternStanding2,
|
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) {
|
fn handle_debug_column(server: &mut Server, entity: EcsEntity, args: String, action: &ChatCommand) {
|
||||||
let sim = server.world.sim();
|
let sim = server.world.sim();
|
||||||
let sampler = server.world.sample_columns();
|
let sampler = server.world.sample_columns();
|
||||||
@ -1120,10 +1088,11 @@ fn handle_remove_lights(
|
|||||||
match opt_player_pos {
|
match opt_player_pos {
|
||||||
Some(player_pos) => {
|
Some(player_pos) => {
|
||||||
let ecs = server.state.ecs();
|
let ecs = server.state.ecs();
|
||||||
for (entity, pos, _, _) in (
|
for (entity, pos, _, _, _) in (
|
||||||
&ecs.entities(),
|
&ecs.entities(),
|
||||||
&ecs.read_storage::<comp::Pos>(),
|
&ecs.read_storage::<comp::Pos>(),
|
||||||
&ecs.read_storage::<comp::LightEmitter>(),
|
&ecs.read_storage::<comp::LightEmitter>(),
|
||||||
|
!&ecs.read_storage::<comp::WaypointArea>(),
|
||||||
!&ecs.read_storage::<comp::Player>(),
|
!&ecs.read_storage::<comp::Player>(),
|
||||||
)
|
)
|
||||||
.join()
|
.join()
|
||||||
|
@ -10,6 +10,8 @@ pub mod input;
|
|||||||
pub mod metrics;
|
pub mod metrics;
|
||||||
pub mod settings;
|
pub mod settings;
|
||||||
pub mod sys;
|
pub mod sys;
|
||||||
|
#[cfg(not(feature = "worldgen"))]
|
||||||
|
mod test_world;
|
||||||
|
|
||||||
// Reexports
|
// Reexports
|
||||||
pub use crate::{error::Error, input::Input, settings::ServerSettings};
|
pub use crate::{error::Error, input::Input, settings::ServerSettings};
|
||||||
@ -44,12 +46,16 @@ use std::{
|
|||||||
sync::Arc,
|
sync::Arc,
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
#[cfg(not(feature = "worldgen"))]
|
||||||
|
use test_world::{World, WORLD_SIZE};
|
||||||
use uvth::{ThreadPool, ThreadPoolBuilder};
|
use uvth::{ThreadPool, ThreadPoolBuilder};
|
||||||
use vek::*;
|
use vek::*;
|
||||||
|
#[cfg(feature = "worldgen")]
|
||||||
use world::{
|
use world::{
|
||||||
sim::{FileOpts, WorldOpts, DEFAULT_WORLD_MAP, WORLD_SIZE},
|
sim::{FileOpts, WorldOpts, DEFAULT_WORLD_MAP, WORLD_SIZE},
|
||||||
World,
|
World,
|
||||||
};
|
};
|
||||||
|
|
||||||
const CLIENT_TIMEOUT: f64 = 20.0; // Seconds
|
const CLIENT_TIMEOUT: f64 = 20.0; // Seconds
|
||||||
|
|
||||||
pub enum Event {
|
pub enum Event {
|
||||||
@ -108,6 +114,7 @@ impl Server {
|
|||||||
state.ecs_mut().register::<RegionSubscription>();
|
state.ecs_mut().register::<RegionSubscription>();
|
||||||
state.ecs_mut().register::<Client>();
|
state.ecs_mut().register::<Client>();
|
||||||
|
|
||||||
|
#[cfg(feature = "worldgen")]
|
||||||
let world = World::generate(
|
let world = World::generate(
|
||||||
settings.world_seed,
|
settings.world_seed,
|
||||||
WorldOpts {
|
WorldOpts {
|
||||||
@ -121,8 +128,15 @@ impl Server {
|
|||||||
..WorldOpts::default()
|
..WorldOpts::default()
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
#[cfg(feature = "worldgen")]
|
||||||
let map = world.sim().get_map();
|
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 = {
|
let spawn_point = {
|
||||||
// NOTE: all of these `.map(|e| e as [type])` calls should compile into no-ops,
|
// 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)
|
// 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
|
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
|
// set the spawn point we calculated above
|
||||||
state.ecs_mut().insert(SpawnPoint(spawn_point));
|
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::Ori(Vec3::unit_y()));
|
||||||
state.write_component(entity, comp::Gravity(1.0));
|
state.write_component(entity, comp::Gravity(1.0));
|
||||||
state.write_component(entity, comp::CharacterState::default());
|
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::Inventory::default());
|
||||||
state.write_component(entity, comp::InventoryUpdate);
|
state.write_component(entity, comp::InventoryUpdate);
|
||||||
// Make sure physics are accepted.
|
// Make sure physics are accepted.
|
||||||
@ -818,12 +836,25 @@ impl Server {
|
|||||||
stats,
|
stats,
|
||||||
body,
|
body,
|
||||||
agent,
|
agent,
|
||||||
|
alignment,
|
||||||
scale,
|
scale,
|
||||||
} => {
|
} => {
|
||||||
state
|
state
|
||||||
.create_npc(pos, stats, body)
|
.create_npc(pos, stats, body)
|
||||||
.with(agent)
|
.with(agent)
|
||||||
.with(scale)
|
.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();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -940,7 +971,7 @@ impl Server {
|
|||||||
(
|
(
|
||||||
&self.state.ecs().entities(),
|
&self.state.ecs().entities(),
|
||||||
&self.state.ecs().read_storage::<comp::Pos>(),
|
&self.state.ecs().read_storage::<comp::Pos>(),
|
||||||
&self.state.ecs().read_storage::<comp::Agent>(),
|
!&self.state.ecs().read_storage::<comp::Player>(),
|
||||||
)
|
)
|
||||||
.join()
|
.join()
|
||||||
.filter(|(_, pos, _)| terrain.get(pos.0.map(|e| e.floor() as i32)).is_err())
|
.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(comp::Controller::default())
|
||||||
.with(body)
|
.with(body)
|
||||||
.with(stats)
|
.with(stats)
|
||||||
|
.with(comp::Alignment::Npc)
|
||||||
.with(comp::Energy::new(500))
|
.with(comp::Energy::new(500))
|
||||||
.with(comp::Gravity(1.0))
|
.with(comp::Gravity(1.0))
|
||||||
.with(comp::CharacterState::default())
|
.with(comp::CharacterState::default())
|
||||||
|
@ -4,6 +4,7 @@ pub mod sentinel;
|
|||||||
pub mod subscription;
|
pub mod subscription;
|
||||||
pub mod terrain;
|
pub mod terrain;
|
||||||
pub mod terrain_sync;
|
pub mod terrain_sync;
|
||||||
|
pub mod waypoint;
|
||||||
|
|
||||||
use specs::DispatcherBuilder;
|
use specs::DispatcherBuilder;
|
||||||
use std::{marker::PhantomData, time::Instant};
|
use std::{marker::PhantomData, time::Instant};
|
||||||
@ -21,6 +22,7 @@ const SENTINEL_SYS: &str = "sentinel_sys";
|
|||||||
const SUBSCRIPTION_SYS: &str = "server_subscription_sys";
|
const SUBSCRIPTION_SYS: &str = "server_subscription_sys";
|
||||||
const TERRAIN_SYNC_SYS: &str = "server_terrain_sync_sys";
|
const TERRAIN_SYNC_SYS: &str = "server_terrain_sync_sys";
|
||||||
const TERRAIN_SYS: &str = "server_terrain_sys";
|
const TERRAIN_SYS: &str = "server_terrain_sys";
|
||||||
|
const WAYPOINT_SYS: &str = "waypoint_sys";
|
||||||
|
|
||||||
pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) {
|
pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) {
|
||||||
// TODO: makes some of these dependent on systems in common like the phys system
|
// 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_sync::Sys, TERRAIN_SYS, &[]);
|
||||||
dispatch_builder.add(terrain::Sys, 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
|
/// Used to keep track of how much time each system takes
|
||||||
|
@ -4,6 +4,7 @@ use common::{
|
|||||||
assets,
|
assets,
|
||||||
comp::{self, item, Player, Pos},
|
comp::{self, item, Player, Pos},
|
||||||
event::{EventBus, ServerEvent},
|
event::{EventBus, ServerEvent},
|
||||||
|
generation::EntityKind,
|
||||||
msg::ServerMsg,
|
msg::ServerMsg,
|
||||||
state::TerrainChanges,
|
state::TerrainChanges,
|
||||||
terrain::TerrainGrid,
|
terrain::TerrainGrid,
|
||||||
@ -95,87 +96,102 @@ impl<'a> System<'a> for Sys {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle chunk supplement
|
// Handle chunk supplement
|
||||||
for npc in supplement.npcs {
|
for entity in supplement.entities {
|
||||||
const SPAWN_NPCS: &'static [fn() -> (String, comp::Body, Option<comp::Item>)] = &[
|
if let EntityKind::Waypoint = entity.kind {
|
||||||
(|| {
|
server_emitter.emit(ServerEvent::CreateWaypoint(entity.pos));
|
||||||
(
|
} else {
|
||||||
"Traveler".into(),
|
const SPAWN_NPCS: &'static [fn() -> (
|
||||||
comp::Body::Humanoid(comp::humanoid::Body::random()),
|
String,
|
||||||
Some(assets::load_expect_cloned("common.items.weapons.staff_1")),
|
comp::Body,
|
||||||
)
|
Option<comp::Item>,
|
||||||
}) as _,
|
comp::Alignment,
|
||||||
(|| {
|
)] = &[
|
||||||
(
|
(|| {
|
||||||
"Wolf".into(),
|
(
|
||||||
comp::Body::QuadrupedMedium(comp::quadruped_medium::Body::random()),
|
"Traveler".into(),
|
||||||
None,
|
comp::Body::Humanoid(comp::humanoid::Body::random()),
|
||||||
)
|
Some(assets::load_expect_cloned("common.items.weapons.staff_1")),
|
||||||
}) as _,
|
comp::Alignment::Enemy,
|
||||||
(|| {
|
)
|
||||||
(
|
}) as _,
|
||||||
"Duck".into(),
|
(|| {
|
||||||
comp::Body::BirdMedium(comp::bird_medium::Body::random()),
|
(
|
||||||
None,
|
"Wolf".into(),
|
||||||
)
|
comp::Body::QuadrupedMedium(comp::quadruped_medium::Body::random()),
|
||||||
}) as _,
|
None,
|
||||||
(|| {
|
comp::Alignment::Enemy,
|
||||||
(
|
)
|
||||||
"Rat".into(),
|
}) as _,
|
||||||
comp::Body::Critter(comp::critter::Body::random()),
|
(|| {
|
||||||
None,
|
(
|
||||||
)
|
"Duck".into(),
|
||||||
}) as _,
|
comp::Body::BirdMedium(comp::bird_medium::Body::random()),
|
||||||
(|| {
|
None,
|
||||||
(
|
comp::Alignment::Wild,
|
||||||
"Pig".into(),
|
)
|
||||||
comp::Body::QuadrupedSmall(comp::quadruped_small::Body::random()),
|
}) as _,
|
||||||
None,
|
(|| {
|
||||||
)
|
(
|
||||||
}),
|
"Rat".into(),
|
||||||
];
|
comp::Body::Critter(comp::critter::Body::random()),
|
||||||
let (name, mut body, main) = SPAWN_NPCS
|
None,
|
||||||
.choose(&mut rand::thread_rng())
|
comp::Alignment::Wild,
|
||||||
.expect("SPAWN_NPCS is nonempty")(
|
)
|
||||||
);
|
}) as _,
|
||||||
let mut stats = comp::Stats::new(name, body, main);
|
(|| {
|
||||||
|
(
|
||||||
|
"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
|
// 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));
|
stats.level.set_level(rand::thread_rng().gen_range(1, 4));
|
||||||
|
|
||||||
if npc.boss {
|
if let EntityKind::Boss = entity.kind {
|
||||||
if rand::random::<f32>() < 0.8 {
|
if rand::random::<f32>() < 0.8 {
|
||||||
let hbody = comp::humanoid::Body::random();
|
let hbody = comp::humanoid::Body::random();
|
||||||
body = comp::Body::Humanoid(hbody);
|
body = comp::Body::Humanoid(hbody);
|
||||||
stats = comp::Stats::new(
|
stats = comp::Stats::new(
|
||||||
"Fearless Wanderer".to_string(),
|
"Fearless Wanderer".to_string(),
|
||||||
body,
|
body,
|
||||||
Some(assets::load_expect_cloned("common.items.weapons.hammer_1")),
|
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.update_max_hp();
|
||||||
stats
|
stats
|
||||||
.health
|
.health
|
||||||
.set_to(stats.health.maximum(), comp::HealthSource::Revive);
|
.set_to(stats.health.maximum(), comp::HealthSource::Revive);
|
||||||
if let Some(item::Item {
|
if let Some(item::Item {
|
||||||
kind: item::ItemKind::Tool { power, .. },
|
kind: item::ItemKind::Tool { power, .. },
|
||||||
..
|
..
|
||||||
}) = &mut stats.equipment.main
|
}) = &mut stats.equipment.main
|
||||||
{
|
{
|
||||||
*power = stats.level.level() * 3;
|
*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::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::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::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::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::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)),
|
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 (
|
for (entity, pos, ori, scale, body, character, last_character, stats) in (
|
||||||
&ecs.entities(),
|
&ecs.entities(),
|
||||||
&ecs.read_storage::<Pos>(),
|
&ecs.read_storage::<Pos>(),
|
||||||
&ecs.read_storage::<Ori>(),
|
ecs.read_storage::<Ori>().maybe(),
|
||||||
ecs.read_storage::<Scale>().maybe(),
|
ecs.read_storage::<Scale>().maybe(),
|
||||||
&ecs.read_storage::<Body>(),
|
&ecs.read_storage::<Body>(),
|
||||||
ecs.read_storage::<CharacterState>().maybe(),
|
ecs.read_storage::<CharacterState>().maybe(),
|
||||||
@ -121,6 +121,8 @@ impl FigureMgr {
|
|||||||
)
|
)
|
||||||
.join()
|
.join()
|
||||||
{
|
{
|
||||||
|
let ori = ori.copied().unwrap_or(Ori(Vec3::unit_y()));
|
||||||
|
|
||||||
// Don't process figures outside the vd
|
// Don't process figures outside the vd
|
||||||
let vd_frac = Vec2::from(pos.0 - player_pos)
|
let vd_frac = Vec2::from(pos.0 - player_pos)
|
||||||
.map2(TerrainChunk::RECT_SIZE, |d: f32, sz| {
|
.map2(TerrainChunk::RECT_SIZE, |d: f32, sz| {
|
||||||
@ -1225,7 +1227,7 @@ impl FigureMgr {
|
|||||||
for (entity, _, _, body, stats, _) in (
|
for (entity, _, _, body, stats, _) in (
|
||||||
&ecs.entities(),
|
&ecs.entities(),
|
||||||
&ecs.read_storage::<Pos>(),
|
&ecs.read_storage::<Pos>(),
|
||||||
&ecs.read_storage::<Ori>(),
|
ecs.read_storage::<Ori>().maybe(),
|
||||||
&ecs.read_storage::<Body>(),
|
&ecs.read_storage::<Body>(),
|
||||||
ecs.read_storage::<Stats>().maybe(),
|
ecs.read_storage::<Stats>().maybe(),
|
||||||
ecs.read_storage::<Scale>().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));
|
.fold(i32::MIN, |max, (_, chunk)| chunk.get_max_z().max(max));
|
||||||
|
|
||||||
let aabb = Aabb {
|
let aabb = Aabb {
|
||||||
min: Vec3::from(aabr.min) + Vec3::unit_z() * (min_z - 1),
|
min: Vec3::from(aabr.min) + Vec3::unit_z() * (min_z - 2),
|
||||||
max: Vec3::from(aabr.max) + Vec3::unit_z() * (max_z + 1),
|
max: Vec3::from(aabr.max) + Vec3::unit_z() * (max_z + 2),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Clone various things so that they can be moved into the thread.
|
// Clone various things so that they can be moved into the thread.
|
||||||
|
@ -19,6 +19,7 @@ use crate::{
|
|||||||
util::Sampler,
|
util::Sampler,
|
||||||
};
|
};
|
||||||
use common::{
|
use common::{
|
||||||
|
generation::{ChunkSupplement, EntityInfo, EntityKind},
|
||||||
terrain::{Block, BlockKind, TerrainChunk, TerrainChunkMeta, TerrainChunkSize},
|
terrain::{Block, BlockKind, TerrainChunk, TerrainChunkMeta, TerrainChunkSize},
|
||||||
vol::{ReadVol, RectVolSize, Vox, WriteVol},
|
vol::{ReadVol, RectVolSize, Vox, WriteVol},
|
||||||
};
|
};
|
||||||
@ -148,35 +149,31 @@ impl World {
|
|||||||
|
|
||||||
const SPAWN_RATE: f32 = 0.1;
|
const SPAWN_RATE: f32 = 0.1;
|
||||||
const BOSS_RATE: f32 = 0.03;
|
const BOSS_RATE: f32 = 0.03;
|
||||||
let supplement = ChunkSupplement {
|
let mut supplement = ChunkSupplement {
|
||||||
npcs: if rand::thread_rng().gen::<f32>() < SPAWN_RATE
|
entities: if rand::thread_rng().gen::<f32>() < SPAWN_RATE
|
||||||
&& sim_chunk.chaos < 0.5
|
&& sim_chunk.chaos < 0.5
|
||||||
&& !sim_chunk.is_underwater
|
&& !sim_chunk.is_underwater
|
||||||
{
|
{
|
||||||
vec![NpcInfo {
|
vec![EntityInfo {
|
||||||
pos: gen_entity_pos(),
|
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 {
|
} else {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if sim_chunk.contains_waypoint {
|
||||||
|
supplement = supplement.with_entity(EntityInfo {
|
||||||
|
pos: gen_entity_pos(),
|
||||||
|
kind: EntityKind::Waypoint,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Ok((chunk, supplement))
|
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;
|
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.rng = rng;
|
||||||
self.locations = locations;
|
self.locations = locations;
|
||||||
}
|
}
|
||||||
@ -1679,6 +1715,7 @@ pub struct SimChunk {
|
|||||||
pub is_underwater: bool,
|
pub is_underwater: bool,
|
||||||
|
|
||||||
pub structures: Structures,
|
pub structures: Structures,
|
||||||
|
pub contains_waypoint: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone)]
|
||||||
@ -1922,6 +1959,7 @@ impl SimChunk {
|
|||||||
location: None,
|
location: None,
|
||||||
river,
|
river,
|
||||||
structures: Structures { town: None },
|
structures: Structures { town: None },
|
||||||
|
contains_waypoint: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user