Merge branch 'zesterer/worldgen-npcs' into 'master'

NPC Spawning

See merge request veloren/veloren!393
This commit is contained in:
Joshua Barretto 2019-08-03 06:41:17 +00:00
commit 62dea9ee7b
13 changed files with 175 additions and 35 deletions

View File

@ -10,10 +10,20 @@ pub enum Agent {
offset: Vec2<f32>,
},
Enemy {
bearing: Vec2<f32>,
target: Option<EcsEntity>,
},
}
impl Agent {
pub fn enemy() -> Self {
Agent::Enemy {
bearing: Vec2::zero(),
target: None,
}
}
}
impl Component for Agent {
type Storage = IDVStorage<Self>;
}

View File

@ -22,7 +22,7 @@ pub use inputs::{
};
pub use inventory::{item, Inventory, InventoryUpdate, Item};
pub use last::Last;
pub use phys::{ForceUpdate, Ori, Pos, Vel};
pub use phys::{ForceUpdate, Ori, Pos, Scale, Vel};
pub use player::Player;
pub use stats::{Dying, Exp, HealthSource, Level, Stats};
pub use visual::LightEmitter;

View File

@ -1,4 +1,4 @@
use specs::{Component, NullStorage};
use specs::{Component, FlaggedStorage, NullStorage};
use specs_idvs::IDVStorage;
use vek::*;
@ -26,6 +26,14 @@ impl Component for Ori {
type Storage = IDVStorage<Self>;
}
// Scale
#[derive(Copy, Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct Scale(pub f32);
impl Component for Scale {
type Storage = FlaggedStorage<Self, IDVStorage<Self>>;
}
// ForceUpdate
#[derive(Copy, Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct ForceUpdate;

View File

@ -34,18 +34,22 @@ impl Health {
pub fn current(&self) -> u32 {
self.current
}
pub fn maximum(&self) -> u32 {
self.maximum
}
pub fn set_to(&mut self, amount: u32, cause: HealthSource) {
let amount = amount.min(self.maximum);
self.last_change = Some((amount as i32 - self.current as i32, 0.0, cause));
self.current = amount;
}
pub fn change_by(&mut self, amount: i32, cause: HealthSource) {
self.current = ((self.current as i32 + amount).max(0) as u32).min(self.maximum);
self.last_change = Some((amount, 0.0, cause));
}
pub fn set_maximum(&mut self, amount: u32) {
self.maximum = amount;
self.current = self.current.min(self.maximum);
@ -132,6 +136,12 @@ impl Stats {
is_dead: false,
}
}
pub fn with_max_health(mut self, amount: u32) -> Self {
self.health.maximum = amount;
self.health.current = amount;
self
}
}
impl Component for Stats {

View File

@ -26,6 +26,7 @@ sphynx::sum_type! {
Stats(comp::Stats),
LightEmitter(comp::LightEmitter),
Item(comp::Item),
Scale(comp::Scale),
}
}
// Automatically derive From<T> for EcsCompPhantom
@ -42,6 +43,7 @@ sphynx::sum_type! {
Stats(PhantomData<comp::Stats>),
LightEmitter(PhantomData<comp::LightEmitter>),
Item(PhantomData<comp::Item>),
Scale(PhantomData<comp::Scale>),
}
}
impl sphynx::CompPacket for EcsCompPacket {

View File

@ -132,6 +132,7 @@ impl State {
ecs.register_synced::<comp::CanBuild>();
ecs.register_synced::<comp::LightEmitter>();
ecs.register_synced::<comp::Item>();
ecs.register_synced::<comp::Scale>();
// Register components send from clients -> server
ecs.register::<comp::Controller>();

View File

@ -58,21 +58,25 @@ impl<'a> System<'a> for Sys {
* 10.0;
}
}
Agent::Enemy { target } => {
Agent::Enemy { bearing, target } => {
const SIGHT_DIST: f32 = 30.0;
let choose_new = match target.map(|tgt| positions.get(tgt)).flatten() {
Some(tgt_pos) => {
let dist = Vec2::<f32>::from(tgt_pos.0 - pos.0).magnitude();
if dist < 2.0 {
controller.move_dir = Vec2::zero();
if rand::random::<f32>() < 0.2 {
if rand::random::<f32>() < 0.05 {
controller.attack = true;
} else {
controller.attack = false;
}
false
} else if dist < 60.0 {
} else if dist < SIGHT_DIST {
controller.move_dir =
Vec2::<f32>::from(tgt_pos.0 - pos.0).normalized() * 0.96;
Vec2::<f32>::from(tgt_pos.0 - pos.0).normalized();
false
} else {
@ -80,16 +84,25 @@ impl<'a> System<'a> for Sys {
}
}
None => {
controller.move_dir = Vec2::one();
*bearing +=
Vec2::new(rand::random::<f32>() - 0.5, rand::random::<f32>() - 0.5)
* 0.1
- *bearing * 0.005;
controller.move_dir = if bearing.magnitude_squared() > 0.1 {
bearing.normalized()
} else {
Vec2::zero()
};
true
}
};
if choose_new {
if choose_new && rand::random::<f32>() < 0.1 {
let entities = (&entities, &positions)
.join()
.filter(|(e, e_pos)| {
Vec2::<f32>::from(e_pos.0 - pos.0).magnitude() < 30.0
Vec2::<f32>::from(e_pos.0 - pos.0).magnitude() < SIGHT_DIST
&& *e != entity
})
.map(|(e, _)| e)

View File

@ -438,7 +438,7 @@ fn handle_help(server: &mut Server, entity: EcsEntity, _args: String, _action: &
fn alignment_to_agent(alignment: &str, target: EcsEntity) -> Option<comp::Agent> {
match alignment {
"hostile" => Some(comp::Agent::Enemy { target: None }),
"hostile" => Some(comp::Agent::enemy()),
"friendly" => Some(comp::Agent::Pet {
target,
offset: Vec2::zero(),

View File

@ -19,8 +19,8 @@ use common::{
net::PostOffice,
state::{State, TimeOfDay, Uid},
terrain::{block::Block, TerrainChunk, TerrainChunkSize, TerrainMap},
vol::VolSize,
vol::Vox,
vol::{ReadVol, VolSize},
};
use log::debug;
use rand::Rng;
@ -34,7 +34,7 @@ use std::{
};
use uvth::{ThreadPool, ThreadPoolBuilder};
use vek::*;
use world::World;
use world::{ChunkSupplement, World};
const CLIENT_TIMEOUT: f64 = 20.0; // Seconds
@ -62,8 +62,8 @@ pub struct Server {
clients: Clients,
thread_pool: ThreadPool,
chunk_tx: mpsc::Sender<(Vec2<i32>, TerrainChunk)>,
chunk_rx: mpsc::Receiver<(Vec2<i32>, TerrainChunk)>,
chunk_tx: mpsc::Sender<(Vec2<i32>, (TerrainChunk, ChunkSupplement))>,
chunk_rx: mpsc::Receiver<(Vec2<i32>, (TerrainChunk, ChunkSupplement))>,
pending_chunks: HashSet<Vec2<i32>>,
server_settings: ServerSettings,
@ -237,7 +237,7 @@ impl Server {
// 5) Fetch any generated `TerrainChunk`s and insert them into the terrain.
// Also, send the chunk data to anybody that is close by.
if let Ok((key, chunk)) = self.chunk_rx.try_recv() {
if let Ok((key, (chunk, supplement))) = self.chunk_rx.try_recv() {
// Send the chunk to all nearby players.
for (entity, view_distance, pos) in (
&self.state.ecs().entities(),
@ -267,6 +267,36 @@ impl Server {
self.state.insert_chunk(key, chunk);
self.pending_chunks.remove(&key);
// Handle chunk supplement
for npc in supplement.npcs {
let mut stats = comp::Stats::new("Wolf".to_string());
let mut body = comp::Body::QuadrupedMedium(comp::quadruped_medium::Body::random());
let mut scale = 1.0;
if npc.boss {
if rand::random::<f32>() < 0.8 {
stats = comp::Stats::new("Humanoid".to_string());
body = comp::Body::Humanoid(comp::humanoid::Body::random());
}
stats = stats.with_max_health(300 + rand::random::<u32>() % 400);
scale = 1.8 + rand::random::<f32>();
}
self.state
.ecs_mut()
.create_entity_synced()
.with(comp::Pos(npc.pos))
.with(comp::Vel(Vec3::zero()))
.with(comp::Ori(Vec3::unit_y()))
.with(comp::Controller::default())
.with(body)
.with(stats)
.with(comp::ActionState::default())
.with(comp::Agent::enemy())
.with(comp::Scale(scale))
.build();
}
}
fn chunk_in_vd(
@ -346,6 +376,7 @@ impl Server {
}
}
}
// Sync changed blocks
let msg =
ServerMsg::TerrainBlockUpdates(self.state.terrain_changes().modified_blocks.clone());
@ -360,6 +391,23 @@ impl Server {
}
}
// Remove NPCs that are outside the view distances of all players
let to_delete = {
let terrain = self.state.terrain();
(
&self.state.ecs().entities(),
&self.state.ecs().read_storage::<comp::Pos>(),
&self.state.ecs().read_storage::<comp::Agent>(),
)
.join()
.filter(|(_, pos, _)| terrain.get(pos.0.map(|e| e.floor() as i32)).is_err())
.map(|(entity, _, _)| entity)
.collect::<Vec<_>>()
};
for entity in to_delete {
let _ = self.state.ecs_mut().delete_entity(entity);
}
// 7) Finish the tick, pass control back to the frontend.
// Cleanup

View File

@ -124,6 +124,7 @@ impl Scene {
renderer,
Vec3::zero(),
-Vec3::unit_y(),
1.0,
Rgba::broadcast(1.0),
1.0 / 60.0, // TODO: Use actual deltatime here?
);

View File

@ -624,11 +624,12 @@ impl FigureMgr {
.get(client.entity())
.map_or(Vec3::zero(), |pos| pos.0);
for (entity, pos, vel, ori, body, animation_info, stats) in (
for (entity, pos, vel, ori, scale, body, animation_info, stats) in (
&ecs.entities(),
&ecs.read_storage::<comp::Pos>(),
&ecs.read_storage::<comp::Vel>(),
&ecs.read_storage::<comp::Ori>(),
ecs.read_storage::<comp::Scale>().maybe(),
&ecs.read_storage::<comp::Body>(),
ecs.read_storage::<comp::AnimationInfo>().maybe(),
ecs.read_storage::<comp::Stats>().maybe(),
@ -671,6 +672,8 @@ impl FigureMgr {
})
.unwrap_or(Rgba::broadcast(1.0));
let scale = scale.map(|s| s.0).unwrap_or(1.0);
let skeleton_attr = &self
.model_cache
.get_or_create_model(renderer, *body, tick)
@ -750,7 +753,7 @@ impl FigureMgr {
};
state.skeleton.interpolate(&target_skeleton, dt);
state.update(renderer, pos.0, ori.0, col, dt);
state.update(renderer, pos.0, ori.0, scale, col, dt);
}
Body::Quadruped(_) => {
let state = self
@ -788,7 +791,7 @@ impl FigureMgr {
};
state.skeleton.interpolate(&target_skeleton, dt);
state.update(renderer, pos.0, ori.0, col, dt);
state.update(renderer, pos.0, ori.0, scale, col, dt);
}
Body::QuadrupedMedium(_) => {
let state = self
@ -834,7 +837,7 @@ impl FigureMgr {
};
state.skeleton.interpolate(&target_skeleton, dt);
state.update(renderer, pos.0, ori.0, col, dt);
state.update(renderer, pos.0, ori.0, scale, col, dt);
}
Body::Object(_) => {
let state = self
@ -843,7 +846,7 @@ impl FigureMgr {
.or_insert_with(|| FigureState::new(renderer, ObjectSkeleton::new()));
state.skeleton = state.skeleton_mut().clone();
state.update(renderer, pos.0, ori.0, col, dt);
state.update(renderer, pos.0, ori.0, scale, col, dt);
}
}
}
@ -967,6 +970,7 @@ impl<S: Skeleton> FigureState<S> {
renderer: &mut Renderer,
pos: Vec3<f32>,
ori: Vec3<f32>,
scale: f32,
col: Rgba<f32>,
dt: f32,
) {
@ -982,7 +986,7 @@ impl<S: Skeleton> FigureState<S> {
let mat = Mat4::<f32>::identity()
* Mat4::translation_3d(self.pos)
* Mat4::rotation_z(-ori.x.atan2(ori.y))
* Mat4::scaling_3d(Vec3::from(0.8));
* Mat4::scaling_3d(Vec3::from(0.8 * scale));
let locals = FigureLocals::new(mat, col);
renderer.update_consts(&mut self.locals, &[locals]).unwrap();

View File

@ -43,7 +43,7 @@ pub fn structure_gen<'a>(
let wheight = st_sample.alt.max(cliff_height);
let st_pos3d = Vec3::new(st_pos.x, st_pos.y, wheight as i32);
let volumes: &'static [_] = if QUIRKY_RAND.get(st_seed) % 64 == 17 {
let volumes: &'static [_] = if QUIRKY_RAND.get(st_seed) % 512 == 17 {
if st_sample.temp > CONFIG.desert_temp {
&QUIRKY_DRY
} else {

View File

@ -17,8 +17,9 @@ use crate::{
};
use common::{
terrain::{Block, TerrainChunk, TerrainChunkMeta, TerrainChunkSize},
vol::{VolSize, Vox, WriteVol},
vol::{ReadVol, VolSize, Vox, WriteVol},
};
use rand::Rng;
use std::time::Duration;
use vek::*;
@ -56,7 +57,7 @@ impl World {
BlockGen::new(self, ColumnGen::new(self))
}
pub fn generate_chunk(&self, chunk_pos: Vec2<i32>) -> TerrainChunk {
pub fn generate_chunk(&self, chunk_pos: Vec2<i32>) -> (TerrainChunk, ChunkSupplement) {
let air = Block::empty();
let stone = Block::new(2, Rgb::new(200, 220, 255));
let water = Block::new(5, Rgb::new(100, 150, 255));
@ -72,21 +73,24 @@ impl World {
{
Some((base_z, sim_chunk)) => (base_z as i32, sim_chunk),
None => {
return TerrainChunk::new(
CONFIG.sea_level as i32,
water,
air,
TerrainChunkMeta::void(),
return (
TerrainChunk::new(
CONFIG.sea_level as i32,
water,
air,
TerrainChunkMeta::void(),
),
ChunkSupplement::default(),
)
}
};
let meta = TerrainChunkMeta::new(sim_chunk.get_name(&self.sim), sim_chunk.get_biome());
let mut chunk = TerrainChunk::new(base_z, stone, air, meta);
let mut sampler = self.sample_blocks();
let chunk_block_pos = Vec3::from(chunk_pos) * TerrainChunkSize::SIZE.map(|e| e as i32);
let mut chunk = TerrainChunk::new(base_z, stone, air, meta);
for x in 0..TerrainChunkSize::SIZE.x as i32 {
for y in 0..TerrainChunkSize::SIZE.y as i32 {
let wpos2d = Vec2::new(x, y)
@ -105,8 +109,7 @@ impl World {
for z in min_z as i32..max_z as i32 {
let lpos = Vec3::new(x, y, z);
let wpos =
lpos + Vec3::from(chunk_pos) * TerrainChunkSize::SIZE.map(|e| e as i32);
let wpos = chunk_block_pos + lpos;
if let Some(block) = sampler.get_with_z_cache(wpos, Some(&z_cache)) {
let _ = chunk.set(lpos, block);
@ -115,6 +118,46 @@ impl World {
}
}
chunk
let gen_entity_pos = || {
let lpos2d = Vec2::from(TerrainChunkSize::SIZE)
.map(|sz| rand::thread_rng().gen::<u32>().rem_euclid(sz));
let mut lpos = Vec3::new(lpos2d.x as i32, lpos2d.y as i32, 0);
while chunk.get(lpos).map(|vox| !vox.is_empty()).unwrap_or(false) {
lpos.z += 1;
}
(chunk_block_pos + lpos).map(|e| e as f32) + 0.5
};
const SPAWN_RATE: f32 = 0.1;
const BOSS_RATE: f32 = 0.1;
let supplement = ChunkSupplement {
npcs: if rand::thread_rng().gen::<f32>() < SPAWN_RATE && sim_chunk.chaos < 0.5 {
vec![NpcInfo {
pos: gen_entity_pos(),
boss: rand::thread_rng().gen::<f32>() < BOSS_RATE,
}]
} else {
Vec::new()
},
};
(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() }
}
}