mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Merge branch 'zesterer/worldgen-npcs' into 'master'
NPC Spawning See merge request veloren/veloren!393
This commit is contained in:
commit
62dea9ee7b
@ -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>;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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>();
|
||||
|
@ -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)
|
||||
|
@ -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(),
|
||||
|
@ -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
|
||||
|
@ -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?
|
||||
);
|
||||
|
@ -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();
|
||||
|
@ -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 {
|
||||
|
@ -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() }
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user