mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Better neutral AI, initial waypoint objects
This commit is contained in:
parent
9d7efad526
commit
11193a692a
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.
@ -119,8 +119,8 @@ where
|
||||
}
|
||||
|
||||
pub enum PathResult<T> {
|
||||
None,
|
||||
Exhausted,
|
||||
None(Path<T>),
|
||||
Exhausted(Path<T>),
|
||||
Path(Path<T>),
|
||||
Pending,
|
||||
}
|
||||
@ -134,6 +134,7 @@ pub struct Astar<S: Clone + Eq + Hash> {
|
||||
cheapest_scores: HashMap<S, f32>,
|
||||
final_scores: HashMap<S, f32>,
|
||||
visited: HashSet<S>,
|
||||
lowest_cost: Option<S>,
|
||||
}
|
||||
|
||||
impl<S: Clone + Eq + Hash> Astar<S> {
|
||||
@ -150,6 +151,7 @@ impl<S: Clone + Eq + Hash> Astar<S> {
|
||||
cheapest_scores: std::iter::once((start.clone(), 0.0)).collect(),
|
||||
final_scores: std::iter::once((start.clone(), heuristic(&start))).collect(),
|
||||
visited: std::iter::once(start).collect(),
|
||||
lowest_cost: None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -169,6 +171,7 @@ impl<S: Clone + Eq + Hash> Astar<S> {
|
||||
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 =
|
||||
@ -191,14 +194,24 @@ impl<S: Clone + Eq + Hash> Astar<S> {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return PathResult::None;
|
||||
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
|
||||
PathResult::Exhausted(
|
||||
self.lowest_cost
|
||||
.clone()
|
||||
.map(|lc| self.reconstruct_path_to(lc))
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
} else {
|
||||
PathResult::Pending
|
||||
}
|
||||
|
@ -26,9 +26,10 @@ impl Component for Alignment {
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Agent {
|
||||
pub chaser: Chaser,
|
||||
pub target: Option<EcsEntity>,
|
||||
pub target: Option<(EcsEntity, f64)>,
|
||||
pub owner: Option<EcsEntity>,
|
||||
pub patrol_origin: Option<Vec3<f32>>,
|
||||
pub wander_pos: Option<Vec3<f32>>,
|
||||
}
|
||||
|
||||
impl Agent {
|
||||
@ -36,6 +37,11 @@ impl Agent {
|
||||
self.owner = Some(owner);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_patrol_origin(mut self, origin: Vec3<f32>) -> Self {
|
||||
self.patrol_origin = Some(origin);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for Agent {
|
||||
|
@ -54,6 +54,7 @@ pub enum Body {
|
||||
CraftingBench = 48,
|
||||
BoltFire = 49,
|
||||
ArrowSnake = 50,
|
||||
CampfireLit = 51,
|
||||
}
|
||||
|
||||
impl Body {
|
||||
@ -63,7 +64,7 @@ impl Body {
|
||||
}
|
||||
}
|
||||
|
||||
const ALL_OBJECTS: [Body; 50] = [
|
||||
const ALL_OBJECTS: [Body; 51] = [
|
||||
Body::Arrow,
|
||||
Body::Bomb,
|
||||
Body::Scarecrow,
|
||||
@ -82,6 +83,7 @@ const ALL_OBJECTS: [Body; 50] = [
|
||||
Body::Pumpkin4,
|
||||
Body::Pumpkin5,
|
||||
Body::Campfire,
|
||||
Body::CampfireLit,
|
||||
Body::LanternGround,
|
||||
Body::LanternGroundOpen,
|
||||
Body::LanternStanding,
|
||||
|
@ -20,3 +20,16 @@ impl Waypoint {
|
||||
impl Component for Waypoint {
|
||||
type Storage = FlaggedStorage<Self, IDVStorage<Self>>;
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub struct WaypointArea(f32);
|
||||
|
||||
impl Component for WaypointArea {
|
||||
type Storage = FlaggedStorage<Self, IDVStorage<Self>>;
|
||||
}
|
||||
|
||||
impl Default for WaypointArea {
|
||||
fn default() -> Self {
|
||||
Self(5.0)
|
||||
}
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ pub use energy::{Energy, EnergySource};
|
||||
pub use inputs::CanBuild;
|
||||
pub use inventory::{item, Inventory, InventoryUpdate, Item, ItemKind};
|
||||
pub use last::Last;
|
||||
pub use location::Waypoint;
|
||||
pub use location::{Waypoint, WaypointArea};
|
||||
pub use phys::{ForceUpdate, Gravity, Mass, Ori, PhysicsState, Pos, Scale, Sticky, Vel};
|
||||
pub use player::Player;
|
||||
pub use projectile::Projectile;
|
||||
|
@ -105,6 +105,7 @@ pub enum ServerEvent {
|
||||
alignment: comp::Alignment,
|
||||
scale: comp::Scale,
|
||||
},
|
||||
CreateWaypoint(Vec3<f32>),
|
||||
ClientDisconnect(EcsEntity),
|
||||
ChunkRequest(EcsEntity, Vec2<i32>),
|
||||
ChatCmd(EcsEntity, String),
|
||||
|
@ -1,11 +1,24 @@
|
||||
use vek::*;
|
||||
|
||||
pub struct NpcInfo {
|
||||
pub enum EntityKind {
|
||||
Enemy,
|
||||
Boss,
|
||||
Waypoint,
|
||||
}
|
||||
|
||||
pub struct EntityInfo {
|
||||
pub pos: Vec3<f32>,
|
||||
pub boss: bool,
|
||||
pub kind: EntityKind,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ChunkSupplement {
|
||||
pub npcs: Vec<NpcInfo>,
|
||||
pub entities: Vec<EntityInfo>,
|
||||
}
|
||||
|
||||
impl ChunkSupplement {
|
||||
pub fn with_entity(mut self, entity: EntityInfo) -> Self {
|
||||
self.entities.push(entity);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
@ -9,11 +9,19 @@ use vek::*;
|
||||
|
||||
// Path
|
||||
|
||||
#[derive(Default, Clone, Debug)]
|
||||
#[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 {
|
||||
@ -134,7 +142,7 @@ impl Chaser {
|
||||
self.route = find_path(&mut self.astar, vol, pos, tgt).into();
|
||||
}
|
||||
|
||||
Some(tgt - pos)
|
||||
Some((tgt - pos) * Vec3::new(1.0, 1.0, 0.0))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -225,10 +233,14 @@ where
|
||||
*astar = None;
|
||||
path
|
||||
}
|
||||
PathResult::Pending => Path::default(),
|
||||
_ => {
|
||||
PathResult::None(path) => {
|
||||
*astar = None;
|
||||
Path::default()
|
||||
path
|
||||
}
|
||||
PathResult::Exhausted(path) => {
|
||||
*astar = None;
|
||||
path
|
||||
}
|
||||
PathResult::Pending => Path::default(),
|
||||
}
|
||||
}
|
||||
|
@ -142,6 +142,7 @@ impl State {
|
||||
ecs.register::<comp::Last<comp::CharacterState>>();
|
||||
ecs.register::<comp::Agent>();
|
||||
ecs.register::<comp::Alignment>();
|
||||
ecs.register::<comp::WaypointArea>();
|
||||
ecs.register::<comp::ForceUpdate>();
|
||||
ecs.register::<comp::InventoryUpdate>();
|
||||
ecs.register::<comp::Admin>();
|
||||
|
@ -4,6 +4,7 @@ use crate::{
|
||||
self, Agent, Alignment, CharacterState, Controller, MountState, MovementState::Glide, Pos,
|
||||
Stats,
|
||||
},
|
||||
state::Time,
|
||||
sync::{Uid, UidAllocator},
|
||||
};
|
||||
use rand::{seq::SliceRandom, thread_rng, Rng};
|
||||
@ -18,6 +19,7 @@ pub struct Sys;
|
||||
impl<'a> System<'a> for Sys {
|
||||
type SystemData = (
|
||||
Read<'a, UidAllocator>,
|
||||
Read<'a, Time>,
|
||||
Entities<'a>,
|
||||
ReadStorage<'a, Pos>,
|
||||
ReadStorage<'a, Stats>,
|
||||
@ -33,6 +35,7 @@ impl<'a> System<'a> for Sys {
|
||||
&mut self,
|
||||
(
|
||||
uid_allocator,
|
||||
time,
|
||||
entities,
|
||||
positions,
|
||||
stats,
|
||||
@ -73,21 +76,25 @@ impl<'a> System<'a> for Sys {
|
||||
let mut inputs = &mut controller.inputs;
|
||||
|
||||
const PET_DIST: f32 = 12.0;
|
||||
const PATROL_DIST: f32 = 48.0;
|
||||
const SIGHT_DIST: f32 = 18.0;
|
||||
const PATROL_DIST: f32 = 32.0;
|
||||
const SIGHT_DIST: f32 = 24.0;
|
||||
const MIN_ATTACK_DIST: f32 = 3.25;
|
||||
const CHASE_TIME_MIN: f64 = 4.0;
|
||||
|
||||
let mut chase_tgt = None;
|
||||
let mut choose_target = false;
|
||||
let mut new_target = None;
|
||||
|
||||
if let Some(target) = agent.target {
|
||||
if let Some((target, aggro_time)) = agent.target {
|
||||
// Chase / attack target
|
||||
if let (Some(tgt_pos), stats) = (positions.get(target), stats.get(target)) {
|
||||
if stats.map(|s| s.is_dead).unwrap_or(false) {
|
||||
// Don't target dead entities
|
||||
choose_target = true;
|
||||
} else if pos.0.distance(tgt_pos.0) < SIGHT_DIST {
|
||||
chase_tgt = Some((tgt_pos.0, 1.0, true))
|
||||
} else if pos.0.distance(tgt_pos.0) < SIGHT_DIST
|
||||
|| (time.0 - aggro_time) < CHASE_TIME_MIN
|
||||
{
|
||||
chase_tgt = Some((tgt_pos.0, 1.5, true))
|
||||
} else {
|
||||
// Lose sight of enemies
|
||||
choose_target = true;
|
||||
@ -95,19 +102,22 @@ impl<'a> System<'a> for Sys {
|
||||
} else {
|
||||
choose_target = true;
|
||||
}
|
||||
} else if let Some(owner) = agent.owner {
|
||||
}
|
||||
|
||||
// Return to owner
|
||||
if let Some(owner) = agent.owner {
|
||||
if let Some(tgt_pos) = positions.get(owner) {
|
||||
if pos.0.distance(tgt_pos.0) > PET_DIST || agent.target.is_none() {
|
||||
if pos.0.distance(tgt_pos.0) > PET_DIST {
|
||||
// Follow owner
|
||||
chase_tgt = Some((tgt_pos.0, 6.0, false));
|
||||
} else {
|
||||
} else if agent.target.is_none() {
|
||||
choose_target = thread_rng().gen::<f32>() < 0.02;
|
||||
}
|
||||
} else {
|
||||
agent.owner = None;
|
||||
}
|
||||
} else if let Some(patrol_origin) = agent.patrol_origin {
|
||||
if pos.0.distance(patrol_origin) < PATROL_DIST {
|
||||
if pos.0.distance(patrol_origin) > PATROL_DIST {
|
||||
// Return to patrol origin
|
||||
chase_tgt = Some((patrol_origin, 64.0, false));
|
||||
}
|
||||
@ -120,15 +130,16 @@ impl<'a> System<'a> for Sys {
|
||||
match stats.health.last_change.1.cause {
|
||||
comp::HealthSource::Attack { by } => {
|
||||
if agent.target.is_none() {
|
||||
agent.target = uid_allocator.retrieve_entity_internal(by.id());
|
||||
new_target = uid_allocator.retrieve_entity_internal(by.id());
|
||||
} else if thread_rng().gen::<f32>() < 0.005 {
|
||||
agent.target = uid_allocator.retrieve_entity_internal(by.id());
|
||||
new_target = uid_allocator.retrieve_entity_internal(by.id());
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Choose a new target
|
||||
if choose_target {
|
||||
// Search for new targets
|
||||
let entities = (&entities, &positions, &stats, alignments.maybe())
|
||||
@ -144,7 +155,12 @@ impl<'a> System<'a> for Sys {
|
||||
.map(|(e, _, _, _)| e)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
agent.target = (&entities).choose(&mut thread_rng()).cloned();
|
||||
new_target = (&entities).choose(&mut thread_rng()).cloned();
|
||||
}
|
||||
|
||||
// Update target when attack begins
|
||||
if let Some(tgt) = new_target {
|
||||
agent.target = Some((tgt, time.0));
|
||||
}
|
||||
|
||||
// Chase target
|
||||
@ -154,7 +170,7 @@ impl<'a> System<'a> for Sys {
|
||||
inputs.jump.set_state(bearing.z > 1.0);
|
||||
}
|
||||
|
||||
if pos.0.distance(tgt_pos) < MIN_ATTACK_DIST && aggressive {
|
||||
if aggressive && pos.0.distance(tgt_pos) < MIN_ATTACK_DIST {
|
||||
inputs.look_dir = tgt_pos - pos.0;
|
||||
inputs.move_dir = Vec2::from(tgt_pos - pos.0)
|
||||
.try_normalized()
|
||||
@ -162,110 +178,42 @@ impl<'a> System<'a> for Sys {
|
||||
* 0.01;
|
||||
inputs.primary.set_state(true);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
match agent {
|
||||
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, chaser } => {
|
||||
// Run towards target.
|
||||
if let Some(tgt_pos) = positions.get(*target) {
|
||||
if let Some(bearing) = chaser.chase(&*terrain, pos.0, tgt_pos.0, 5.0) {
|
||||
// We're not wandering
|
||||
agent.wander_pos = None;
|
||||
} else {
|
||||
if let Some(wander_pos) = agent.wander_pos {
|
||||
if pos.0.distance(wander_pos) < 4.0 {
|
||||
agent.wander_pos = None;
|
||||
} else {
|
||||
if let Some(bearing) = agent.chaser.chase(&*terrain, pos.0, wander_pos, 3.0)
|
||||
{
|
||||
inputs.move_dir =
|
||||
Vec2::from(bearing).try_normalized().unwrap_or(Vec2::zero());
|
||||
Vec2::from(bearing).try_normalized().unwrap_or(Vec2::zero()) * 0.5;
|
||||
inputs.jump.set_state(bearing.z > 1.0);
|
||||
}
|
||||
} else {
|
||||
inputs.move_dir = Vec2::zero();
|
||||
}
|
||||
}
|
||||
Agent::Enemy { bearing, target } => {
|
||||
const SIGHT_DIST: f32 = 18.0;
|
||||
const MIN_ATTACK_DIST: f32 = 3.25;
|
||||
let mut choose_new = false;
|
||||
|
||||
if let Some((Some(target_pos), Some(target_stats), Some(target_character))) =
|
||||
target.map(|target| {
|
||||
(
|
||||
positions.get(target),
|
||||
stats.get(target),
|
||||
character_states.get(target),
|
||||
)
|
||||
})
|
||||
{
|
||||
inputs.look_dir = target_pos.0 - pos.0;
|
||||
|
||||
let dist = Vec2::<f32>::from(target_pos.0 - pos.0).magnitude();
|
||||
if target_stats.is_dead {
|
||||
choose_new = true;
|
||||
} else if dist < 0.001 {
|
||||
// Probably can only happen when entities are at a different z-level
|
||||
// since at the same level repulsion would keep them apart.
|
||||
// Distinct from the first if block since we may want to change the
|
||||
// behavior for this case.
|
||||
choose_new = true;
|
||||
} else if dist < MIN_ATTACK_DIST {
|
||||
// Fight (and slowly move closer)
|
||||
inputs.move_dir =
|
||||
Vec2::<f32>::from(target_pos.0 - pos.0).normalized() * 0.01;
|
||||
inputs.primary.set_state(true);
|
||||
} else if dist < SIGHT_DIST {
|
||||
inputs.move_dir =
|
||||
Vec2::<f32>::from(target_pos.0 - pos.0).normalized() * 0.96;
|
||||
|
||||
if rand::random::<f32>() < 0.02 {
|
||||
inputs.roll.set_state(true);
|
||||
}
|
||||
|
||||
if target_character.movement == Glide && target_pos.0.z > pos.0.z + 5.0
|
||||
{
|
||||
inputs.glide.set_state(true);
|
||||
inputs.jump.set_state(true);
|
||||
}
|
||||
// Choose new wander position
|
||||
if agent.wander_pos.is_none() || thread_rng().gen::<f32>() < 0.005 {
|
||||
agent.wander_pos = if thread_rng().gen::<f32>() < 0.5 {
|
||||
let max_dist = if agent.owner.is_some() {
|
||||
PET_DIST
|
||||
} else {
|
||||
choose_new = true;
|
||||
}
|
||||
} else {
|
||||
*bearing +=
|
||||
Vec2::new(rand::random::<f32>() - 0.5, rand::random::<f32>() - 0.5)
|
||||
* 0.1
|
||||
- *bearing * 0.005;
|
||||
|
||||
inputs.move_dir = if bearing.magnitude_squared() > 0.001 {
|
||||
bearing.normalized()
|
||||
} else {
|
||||
Vec2::zero()
|
||||
PATROL_DIST
|
||||
};
|
||||
|
||||
choose_new = true;
|
||||
}
|
||||
|
||||
if choose_new && rand::random::<f32>() < 0.1 {
|
||||
let entities = (&entities, &positions, &stats)
|
||||
.join()
|
||||
.filter(|(e, e_pos, e_stats)| {
|
||||
(e_pos.0 - pos.0).magnitude() < SIGHT_DIST
|
||||
&& *e != entity
|
||||
&& !e_stats.is_dead
|
||||
})
|
||||
.map(|(e, _, _)| e)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut rng = thread_rng();
|
||||
*target = (&entities).choose(&mut rng).cloned();
|
||||
}
|
||||
Some(
|
||||
agent
|
||||
.patrol_origin
|
||||
.unwrap_or(pos.0)
|
||||
.map(|e| e + (thread_rng().gen::<f32>() - 0.5) * max_dist),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
debug_assert!(inputs.move_dir.map(|e| !e.is_nan()).reduce_and());
|
||||
debug_assert!(inputs.look_dir.map(|e| !e.is_nan()).reduce_and());
|
||||
|
@ -9,7 +9,7 @@ worldgen = ["server/worldgen"]
|
||||
default = ["worldgen"]
|
||||
|
||||
[dependencies]
|
||||
server = { package = "veloren-server", path = "../server" }
|
||||
server = { package = "veloren-server", path = "../server", default-features = false }
|
||||
common = { package = "veloren-common", path = "../common" }
|
||||
|
||||
log = "0.4.8"
|
||||
|
@ -657,6 +657,7 @@ fn handle_object(server: &mut Server, entity: EcsEntity, args: String, _action:
|
||||
Ok("pumpkin_4") => comp::object::Body::Pumpkin4,
|
||||
Ok("pumpkin_5") => comp::object::Body::Pumpkin5,
|
||||
Ok("campfire") => comp::object::Body::Campfire,
|
||||
Ok("campfire_lit") => comp::object::Body::CampfireLit,
|
||||
Ok("lantern_ground") => comp::object::Body::LanternGround,
|
||||
Ok("lantern_ground_open") => comp::object::Body::LanternGroundOpen,
|
||||
Ok("lantern_2") => comp::object::Body::LanternStanding2,
|
||||
|
@ -847,6 +847,17 @@ impl Server {
|
||||
.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();
|
||||
}
|
||||
|
||||
ServerEvent::ClientDisconnect(entity) => {
|
||||
// Tell other clients to remove from player list
|
||||
if let (Some(uid), Some(_)) = (
|
||||
|
@ -4,6 +4,7 @@ use common::{
|
||||
assets,
|
||||
comp::{self, item, Player, Pos},
|
||||
event::{EventBus, ServerEvent},
|
||||
generation::EntityKind,
|
||||
msg::ServerMsg,
|
||||
state::TerrainChanges,
|
||||
terrain::TerrainGrid,
|
||||
@ -95,88 +96,92 @@ impl<'a> System<'a> for Sys {
|
||||
}
|
||||
|
||||
// Handle chunk supplement
|
||||
for npc in supplement.npcs {
|
||||
const SPAWN_NPCS: &'static [fn() -> (String, comp::Body, Option<comp::Item>)] = &[
|
||||
(|| {
|
||||
(
|
||||
"Traveler".into(),
|
||||
comp::Body::Humanoid(comp::humanoid::Body::random()),
|
||||
Some(assets::load_expect_cloned("common.items.weapons.staff_1")),
|
||||
)
|
||||
}) as _,
|
||||
(|| {
|
||||
(
|
||||
"Wolf".into(),
|
||||
comp::Body::QuadrupedMedium(comp::quadruped_medium::Body::random()),
|
||||
None,
|
||||
)
|
||||
}) as _,
|
||||
(|| {
|
||||
(
|
||||
"Duck".into(),
|
||||
comp::Body::BirdMedium(comp::bird_medium::Body::random()),
|
||||
None,
|
||||
)
|
||||
}) as _,
|
||||
(|| {
|
||||
(
|
||||
"Rat".into(),
|
||||
comp::Body::Critter(comp::critter::Body::random()),
|
||||
None,
|
||||
)
|
||||
}) as _,
|
||||
(|| {
|
||||
(
|
||||
"Pig".into(),
|
||||
comp::Body::QuadrupedSmall(comp::quadruped_small::Body::random()),
|
||||
None,
|
||||
)
|
||||
}),
|
||||
];
|
||||
let (name, mut body, main) = SPAWN_NPCS
|
||||
.choose(&mut rand::thread_rng())
|
||||
.expect("SPAWN_NPCS is nonempty")(
|
||||
);
|
||||
let mut stats = comp::Stats::new(name, body, main);
|
||||
for entity in supplement.entities {
|
||||
if let EntityKind::Waypoint = entity.kind {
|
||||
server_emitter.emit(ServerEvent::CreateWaypoint(entity.pos));
|
||||
} else {
|
||||
const SPAWN_NPCS: &'static [fn() -> (String, comp::Body, Option<comp::Item>)] = &[
|
||||
(|| {
|
||||
(
|
||||
"Traveler".into(),
|
||||
comp::Body::Humanoid(comp::humanoid::Body::random()),
|
||||
Some(assets::load_expect_cloned("common.items.weapons.staff_1")),
|
||||
)
|
||||
}) as _,
|
||||
(|| {
|
||||
(
|
||||
"Wolf".into(),
|
||||
comp::Body::QuadrupedMedium(comp::quadruped_medium::Body::random()),
|
||||
None,
|
||||
)
|
||||
}) as _,
|
||||
(|| {
|
||||
(
|
||||
"Duck".into(),
|
||||
comp::Body::BirdMedium(comp::bird_medium::Body::random()),
|
||||
None,
|
||||
)
|
||||
}) as _,
|
||||
(|| {
|
||||
(
|
||||
"Rat".into(),
|
||||
comp::Body::Critter(comp::critter::Body::random()),
|
||||
None,
|
||||
)
|
||||
}) as _,
|
||||
(|| {
|
||||
(
|
||||
"Pig".into(),
|
||||
comp::Body::QuadrupedSmall(comp::quadruped_small::Body::random()),
|
||||
None,
|
||||
)
|
||||
}),
|
||||
];
|
||||
let (name, mut body, main) = SPAWN_NPCS
|
||||
.choose(&mut rand::thread_rng())
|
||||
.expect("SPAWN_NPCS is nonempty")(
|
||||
);
|
||||
let mut stats = comp::Stats::new(name, body, main);
|
||||
|
||||
let mut scale = 1.0;
|
||||
let mut scale = 1.0;
|
||||
|
||||
// TODO: Remove this and implement scaling or level depending on stuff like species instead
|
||||
stats.level.set_level(rand::thread_rng().gen_range(1, 4));
|
||||
// TODO: Remove this and implement scaling or level depending on stuff like species instead
|
||||
stats.level.set_level(rand::thread_rng().gen_range(1, 4));
|
||||
|
||||
if npc.boss {
|
||||
if rand::random::<f32>() < 0.8 {
|
||||
let hbody = comp::humanoid::Body::random();
|
||||
body = comp::Body::Humanoid(hbody);
|
||||
stats = comp::Stats::new(
|
||||
"Fearless Wanderer".to_string(),
|
||||
body,
|
||||
Some(assets::load_expect_cloned("common.items.weapons.hammer_1")),
|
||||
);
|
||||
if let EntityKind::Boss = entity.kind {
|
||||
if rand::random::<f32>() < 0.8 {
|
||||
let hbody = comp::humanoid::Body::random();
|
||||
body = comp::Body::Humanoid(hbody);
|
||||
stats = comp::Stats::new(
|
||||
"Fearless Wanderer".to_string(),
|
||||
body,
|
||||
Some(assets::load_expect_cloned("common.items.weapons.hammer_1")),
|
||||
);
|
||||
}
|
||||
stats.level.set_level(rand::thread_rng().gen_range(8, 15));
|
||||
scale = 2.0 + rand::random::<f32>();
|
||||
}
|
||||
stats.level.set_level(rand::thread_rng().gen_range(8, 15));
|
||||
scale = 2.0 + rand::random::<f32>();
|
||||
}
|
||||
|
||||
stats.update_max_hp();
|
||||
stats
|
||||
.health
|
||||
.set_to(stats.health.maximum(), comp::HealthSource::Revive);
|
||||
if let Some(item::Item {
|
||||
kind: item::ItemKind::Tool { power, .. },
|
||||
..
|
||||
}) = &mut stats.equipment.main
|
||||
{
|
||||
*power = stats.level.level() * 3;
|
||||
stats.update_max_hp();
|
||||
stats
|
||||
.health
|
||||
.set_to(stats.health.maximum(), comp::HealthSource::Revive);
|
||||
if let Some(item::Item {
|
||||
kind: item::ItemKind::Tool { power, .. },
|
||||
..
|
||||
}) = &mut stats.equipment.main
|
||||
{
|
||||
*power = stats.level.level() * 3;
|
||||
}
|
||||
server_emitter.emit(ServerEvent::CreateNpc {
|
||||
pos: Pos(entity.pos),
|
||||
stats,
|
||||
body,
|
||||
alignment: comp::Alignment::Enemy,
|
||||
agent: comp::Agent::default().with_patrol_origin(entity.pos),
|
||||
scale: comp::Scale(scale),
|
||||
})
|
||||
}
|
||||
server_emitter.emit(ServerEvent::CreateNpc {
|
||||
pos: Pos(npc.pos),
|
||||
stats,
|
||||
body,
|
||||
alignment: comp::Alignment::Enemy,
|
||||
agent: comp::Agent::default(),
|
||||
scale: comp::Scale(scale),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
use common::{
|
||||
generation::{ChunkSupplement, NpcInfo},
|
||||
generation::{ChunkSupplement, EntityInfo, EntityKind},
|
||||
terrain::{Block, BlockKind, TerrainChunk, TerrainChunkMeta, TerrainChunkSize},
|
||||
vol::{ReadVol, RectVolSize, Vox, WriteVol},
|
||||
};
|
||||
@ -37,7 +37,10 @@ impl World {
|
||||
Block::empty(),
|
||||
TerrainChunkMeta::void(),
|
||||
),
|
||||
ChunkSupplement::default(),
|
||||
ChunkSupplement::default().with_entity(EntityInfo {
|
||||
pos: Vec3::<f32>::from(chunk_pos.map(|e| e as f32 * 32.0)) + Vec3::unit_z() * 256.0,
|
||||
kind: EntityKind::Waypoint,
|
||||
}),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
@ -1713,6 +1713,7 @@ pub fn mesh_object(obj: object::Body) -> Mesh<FigurePipeline> {
|
||||
Body::Pumpkin4 => ("object.pumpkin_4", Vec3::new(-5.0, -4.0, 0.0)),
|
||||
Body::Pumpkin5 => ("object.pumpkin_5", Vec3::new(-4.0, -5.0, 0.0)),
|
||||
Body::Campfire => ("object.campfire", Vec3::new(-9.0, -10.0, 0.0)),
|
||||
Body::CampfireLit => ("object.campfire_lit", Vec3::new(-9.0, -10.0, 0.0)),
|
||||
Body::LanternGround => ("object.lantern_ground", Vec3::new(-3.5, -3.5, 0.0)),
|
||||
Body::LanternGroundOpen => ("object.lantern_ground_open", Vec3::new(-3.5, -3.5, 0.0)),
|
||||
Body::LanternStanding => ("object.lantern_standing", Vec3::new(-7.5, -3.5, 0.0)),
|
||||
|
@ -19,7 +19,7 @@ use crate::{
|
||||
util::Sampler,
|
||||
};
|
||||
use common::{
|
||||
generation::{ChunkSupplement, NpcInfo},
|
||||
generation::{ChunkSupplement, EntityInfo, EntityKind},
|
||||
terrain::{Block, BlockKind, TerrainChunk, TerrainChunkMeta, TerrainChunkSize},
|
||||
vol::{ReadVol, RectVolSize, Vox, WriteVol},
|
||||
};
|
||||
@ -150,13 +150,17 @@ impl World {
|
||||
const SPAWN_RATE: f32 = 0.1;
|
||||
const BOSS_RATE: f32 = 0.03;
|
||||
let supplement = ChunkSupplement {
|
||||
npcs: if rand::thread_rng().gen::<f32>() < SPAWN_RATE
|
||||
entities: if rand::thread_rng().gen::<f32>() < SPAWN_RATE
|
||||
&& sim_chunk.chaos < 0.5
|
||||
&& !sim_chunk.is_underwater
|
||||
{
|
||||
vec![NpcInfo {
|
||||
vec![EntityInfo {
|
||||
pos: gen_entity_pos(),
|
||||
boss: rand::thread_rng().gen::<f32>() < BOSS_RATE,
|
||||
kind: if rand::thread_rng().gen::<f32>() < BOSS_RATE {
|
||||
EntityKind::Boss
|
||||
} else {
|
||||
EntityKind::Enemy
|
||||
},
|
||||
}]
|
||||
} else {
|
||||
Vec::new()
|
||||
|
Loading…
Reference in New Issue
Block a user