mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Make vehicles npcs and npc uid used in rtsim commands
This commit is contained in:
parent
b9bfb3f1dd
commit
1f195fd848
@ -12,7 +12,7 @@ use crate::{
|
||||
lottery::LootSpec,
|
||||
mounting::VolumePos,
|
||||
outcome::Outcome,
|
||||
rtsim::{RtSimEntity, RtSimVehicle},
|
||||
rtsim::RtSimEntity,
|
||||
terrain::SpriteKind,
|
||||
trade::{TradeAction, TradeId},
|
||||
uid::Uid,
|
||||
@ -225,13 +225,15 @@ pub enum ServerEvent {
|
||||
// TODO: to avoid breakage when adding new fields, perhaps have an `NpcBuilder` type?
|
||||
CreateNpc {
|
||||
pos: Pos,
|
||||
ori: Ori,
|
||||
npc: NpcBuilder,
|
||||
rider: Option<NpcBuilder>,
|
||||
},
|
||||
CreateShip {
|
||||
pos: Pos,
|
||||
ori: Ori,
|
||||
ship: comp::ship::Body,
|
||||
rtsim_entity: Option<RtSimVehicle>,
|
||||
rtsim_entity: Option<RtSimEntity>,
|
||||
driver: Option<NpcBuilder>,
|
||||
},
|
||||
CreateWaypoint(Vec3<f32>),
|
||||
|
@ -17,8 +17,6 @@ use vek::*;
|
||||
|
||||
slotmap::new_key_type! { pub struct NpcId; }
|
||||
|
||||
slotmap::new_key_type! { pub struct VehicleId; }
|
||||
|
||||
slotmap::new_key_type! { pub struct SiteId; }
|
||||
|
||||
slotmap::new_key_type! { pub struct FactionId; }
|
||||
@ -32,7 +30,7 @@ impl Component for RtSimEntity {
|
||||
type Storage = specs::VecStorage<Self>;
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||
pub enum Actor {
|
||||
Npc(NpcId),
|
||||
Character(CharacterId),
|
||||
@ -47,11 +45,12 @@ impl Actor {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct RtSimVehicle(pub VehicleId);
|
||||
impl From<NpcId> for Actor {
|
||||
fn from(value: NpcId) -> Self { Actor::Npc(value) }
|
||||
}
|
||||
|
||||
impl Component for RtSimVehicle {
|
||||
type Storage = specs::VecStorage<Self>;
|
||||
impl From<CharacterId> for Actor {
|
||||
fn from(value: CharacterId) -> Self { Actor::Character(value) }
|
||||
}
|
||||
|
||||
#[derive(EnumIter, Clone, Copy)]
|
||||
@ -313,6 +312,8 @@ pub enum Role {
|
||||
Wild,
|
||||
#[serde(rename = "2")]
|
||||
Monster,
|
||||
#[serde(rename = "2")]
|
||||
Vehicle,
|
||||
}
|
||||
|
||||
// Note: the `serde(name = "...")` is to minimise the length of field
|
||||
|
@ -185,9 +185,17 @@ impl CharacterBehavior for Data {
|
||||
is_point: false,
|
||||
});
|
||||
|
||||
let mut rng = rand::thread_rng();
|
||||
// Send server event to create npc
|
||||
output_events.emit_server(ServerEvent::CreateNpc {
|
||||
pos: comp::Pos(collision_vector - Vec3::unit_z() * obstacle_z),
|
||||
ori: crate::util::Dir::from_unnormalized(Vec3::new(
|
||||
rng.gen_range(-1.0..=1.0),
|
||||
rng.gen_range(-1.0..=1.0),
|
||||
0.0,
|
||||
))
|
||||
.map(|dir| comp::Ori::from(dir))
|
||||
.unwrap_or_default(),
|
||||
npc: NpcBuilder::new(stats, body, comp::Alignment::Owned(*data.uid))
|
||||
.with_skill_set(skill_set)
|
||||
.with_health(health)
|
||||
@ -204,6 +212,7 @@ impl CharacterBehavior for Data {
|
||||
.unwrap_or(comp::Scale(1.0)),
|
||||
)
|
||||
.with_projectile(projectile),
|
||||
rider: None,
|
||||
});
|
||||
|
||||
// Send local event used for frontend shenanigans
|
||||
|
@ -29,7 +29,7 @@ use std::{
|
||||
/// Note that this number does *not* need incrementing on every change: most
|
||||
/// field removals/additions are fine. This number should only be incremented
|
||||
/// when we wish to perform a *hard purge* of rtsim data.
|
||||
pub const CURRENT_VERSION: u32 = 4;
|
||||
pub const CURRENT_VERSION: u32 = 5;
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct Data {
|
||||
|
@ -10,7 +10,7 @@ use common::{
|
||||
grid::Grid,
|
||||
rtsim::{
|
||||
Actor, ChunkResource, FactionId, NpcAction, NpcActivity, NpcInput, Personality, ReportId,
|
||||
Role, SiteId, VehicleId,
|
||||
Role, SiteId,
|
||||
},
|
||||
store::Id,
|
||||
terrain::CoordinateConversions,
|
||||
@ -24,6 +24,7 @@ use std::{
|
||||
collections::VecDeque,
|
||||
ops::{Deref, DerefMut},
|
||||
};
|
||||
use tracing::error;
|
||||
use vek::*;
|
||||
use world::{
|
||||
civ::Track,
|
||||
@ -95,19 +96,26 @@ pub struct Brain {
|
||||
pub action: Box<dyn Action<(), !>>,
|
||||
}
|
||||
|
||||
// #[derive(Serialize, Deserialize, Default, Clone)]
|
||||
// pub struct Relations {
|
||||
// #[serde(skip)]
|
||||
// pub driver: Option<Actor>,
|
||||
// }
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Npc {
|
||||
pub uid: u64,
|
||||
// Persisted state
|
||||
pub seed: u32,
|
||||
/// Represents the location of the NPC.
|
||||
pub wpos: Vec3<f32>,
|
||||
pub dir: Vec2<f32>,
|
||||
|
||||
pub body: comp::Body,
|
||||
pub role: Role,
|
||||
pub home: Option<SiteId>,
|
||||
pub faction: Option<FactionId>,
|
||||
pub riding: Option<Riding>,
|
||||
|
||||
// pub relations: Relations,
|
||||
pub is_dead: bool,
|
||||
|
||||
/// The [`Report`]s that the NPC is aware of.
|
||||
@ -143,12 +151,14 @@ pub struct Npc {
|
||||
impl Clone for Npc {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
uid: self.uid,
|
||||
seed: self.seed,
|
||||
wpos: self.wpos,
|
||||
dir: self.dir,
|
||||
role: self.role.clone(),
|
||||
home: self.home,
|
||||
faction: self.faction,
|
||||
riding: self.riding.clone(),
|
||||
// relations: self.relations.clone(),
|
||||
is_dead: self.is_dead,
|
||||
known_reports: self.known_reports.clone(),
|
||||
body: self.body,
|
||||
@ -171,15 +181,18 @@ impl Npc {
|
||||
|
||||
pub fn new(seed: u32, wpos: Vec3<f32>, body: comp::Body, role: Role) -> Self {
|
||||
Self {
|
||||
// To be assigned later
|
||||
uid: 0,
|
||||
seed,
|
||||
wpos,
|
||||
dir: Vec2::unit_x(),
|
||||
body,
|
||||
personality: Default::default(),
|
||||
sentiments: Default::default(),
|
||||
role,
|
||||
home: None,
|
||||
faction: None,
|
||||
riding: None,
|
||||
// relations: Default::default(),
|
||||
is_dead: false,
|
||||
known_reports: Default::default(),
|
||||
chunk_pos: None,
|
||||
@ -213,24 +226,6 @@ impl Npc {
|
||||
self
|
||||
}
|
||||
|
||||
// TODO: have a dedicated `NpcBuilder` type for this.
|
||||
pub fn steering(mut self, vehicle: impl Into<Option<VehicleId>>) -> Self {
|
||||
self.riding = vehicle.into().map(|vehicle| Riding {
|
||||
vehicle,
|
||||
steering: true,
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
// TODO: have a dedicated `NpcBuilder` type for this.
|
||||
pub fn riding(mut self, vehicle: impl Into<Option<VehicleId>>) -> Self {
|
||||
self.riding = vehicle.into().map(|vehicle| Riding {
|
||||
vehicle,
|
||||
steering: false,
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
// TODO: have a dedicated `NpcBuilder` type for this.
|
||||
pub fn with_faction(mut self, faction: impl Into<Option<FactionId>>) -> Self {
|
||||
self.faction = faction.into();
|
||||
@ -246,7 +241,7 @@ impl Npc {
|
||||
pub fn profession(&self) -> Option<Profession> {
|
||||
match &self.role {
|
||||
Role::Civilised(profession) => profession.clone(),
|
||||
Role::Monster | Role::Wild => None,
|
||||
Role::Monster | Role::Wild | Role::Vehicle => None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -264,78 +259,221 @@ impl Npc {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Riding {
|
||||
pub vehicle: VehicleId,
|
||||
pub steering: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub enum VehicleKind {
|
||||
Airship,
|
||||
Boat,
|
||||
}
|
||||
|
||||
// TODO: Merge into `Npc`?
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct Vehicle {
|
||||
pub wpos: Vec3<f32>,
|
||||
pub dir: Vec2<f32>,
|
||||
|
||||
pub body: comp::ship::Body,
|
||||
|
||||
#[serde(skip)]
|
||||
pub chunk_pos: Option<Vec2<i32>>,
|
||||
|
||||
#[serde(skip)]
|
||||
pub driver: Option<Actor>,
|
||||
|
||||
/// Whether the Vehicle is in simulated or loaded mode (when rtsim is run on
|
||||
/// the server, loaded corresponds to being within a loaded chunk). When
|
||||
/// in loaded mode, the interactions of the Vehicle should not be
|
||||
/// simulated but should instead be derived from the game.
|
||||
#[serde(skip)]
|
||||
pub mode: SimulationMode,
|
||||
}
|
||||
|
||||
impl Vehicle {
|
||||
pub fn new(wpos: Vec3<f32>, body: comp::ship::Body) -> Self {
|
||||
Self {
|
||||
wpos,
|
||||
dir: Vec2::unit_y(),
|
||||
body,
|
||||
chunk_pos: None,
|
||||
driver: None,
|
||||
mode: SimulationMode::Simulated,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_body(&self) -> comp::Body { comp::Body::Ship(self.body) }
|
||||
|
||||
/// Max speed in block/s
|
||||
pub fn get_speed(&self) -> f32 {
|
||||
match self.body {
|
||||
comp::ship::Body::DefaultAirship => 7.0,
|
||||
comp::ship::Body::AirBalloon => 8.0,
|
||||
comp::ship::Body::SailBoat => 5.0,
|
||||
comp::ship::Body::Galleon => 6.0,
|
||||
comp::ship::Body::Skiff => 6.0,
|
||||
comp::ship::Body::Submarine => 4.0,
|
||||
_ => 10.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Serialize, Deserialize)]
|
||||
pub struct GridCell {
|
||||
pub npcs: Vec<NpcId>,
|
||||
pub vehicles: Vec<VehicleId>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
pub struct NpcLink {
|
||||
pub mount: NpcId,
|
||||
pub rider: Actor,
|
||||
pub is_steering: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize)]
|
||||
struct Riders {
|
||||
steerer: Option<MountId>,
|
||||
riders: Vec<MountId>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize)]
|
||||
#[serde(
|
||||
from = "HopSlotMap<MountId, NpcLink>",
|
||||
into = "HopSlotMap<MountId, NpcLink>"
|
||||
)]
|
||||
pub struct NpcLinks {
|
||||
links: HopSlotMap<MountId, NpcLink>,
|
||||
mount_map: slotmap::SecondaryMap<NpcId, Riders>,
|
||||
rider_map: HashMap<Actor, MountId>,
|
||||
}
|
||||
|
||||
impl NpcLinks {
|
||||
pub fn remove_mount(&mut self, mount: NpcId) {
|
||||
if let Some(riders) = self.mount_map.remove(mount) {
|
||||
for link in riders
|
||||
.riders
|
||||
.into_iter()
|
||||
.chain(riders.steerer)
|
||||
.filter_map(|link_id| self.links.get(link_id))
|
||||
{
|
||||
self.rider_map.remove(&link.rider);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal function, only removes from `mount_map`.
|
||||
fn remove_rider(&mut self, id: MountId, link: &NpcLink) {
|
||||
if let Some(riders) = self.mount_map.get_mut(link.mount) {
|
||||
if link.is_steering && riders.steerer == Some(id) {
|
||||
riders.steerer = None;
|
||||
} else {
|
||||
if let Some((i, _)) = riders.riders.iter().enumerate().find(|(_, i)| **i == id) {
|
||||
riders.riders.remove(i);
|
||||
}
|
||||
}
|
||||
if riders.steerer.is_none() && riders.riders.is_empty() {
|
||||
self.mount_map.remove(link.mount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_link(&mut self, link_id: MountId) {
|
||||
if let Some(link) = self.links.remove(link_id) {
|
||||
self.rider_map.remove(&link.rider);
|
||||
self.remove_rider(link_id, &link);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dismount(&mut self, rider: impl Into<Actor>) {
|
||||
if let Some(id) = self.rider_map.remove(&rider.into()) {
|
||||
if let Some(link) = self.links.remove(id) {
|
||||
self.remove_rider(id, &link);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This is the only function to actually add a mount link.
|
||||
// And it ensures that there isn't link chaining
|
||||
pub fn add_mounting(
|
||||
&mut self,
|
||||
mount: NpcId,
|
||||
rider: impl Into<Actor>,
|
||||
steering: bool,
|
||||
) -> Result<MountId, MountingError> {
|
||||
let rider = rider.into();
|
||||
if Actor::Npc(mount) == rider {
|
||||
return Err(MountingError::MountSelf);
|
||||
}
|
||||
if let Actor::Npc(rider) = rider && self.mount_map.contains_key(rider) {
|
||||
return Err(MountingError::RiderIsMounted);
|
||||
}
|
||||
if self.rider_map.contains_key(&Actor::Npc(mount)) {
|
||||
return Err(MountingError::MountIsRiding);
|
||||
}
|
||||
if let Some(mount_entry) = self.mount_map.entry(mount) {
|
||||
if let hashbrown::hash_map::Entry::Vacant(rider_entry) = self.rider_map.entry(rider) {
|
||||
let riders = mount_entry.or_insert(Riders::default());
|
||||
|
||||
if steering {
|
||||
if riders.steerer.is_none() {
|
||||
let id = self.links.insert(NpcLink {
|
||||
mount,
|
||||
rider,
|
||||
is_steering: true,
|
||||
});
|
||||
riders.steerer = Some(id);
|
||||
rider_entry.insert(id);
|
||||
Ok(id)
|
||||
} else {
|
||||
Err(MountingError::HasSteerer)
|
||||
}
|
||||
} else {
|
||||
// TODO: Maybe have some limit on the number of riders depending on the mount?
|
||||
let id = self.links.insert(NpcLink {
|
||||
mount,
|
||||
rider,
|
||||
is_steering: false,
|
||||
});
|
||||
riders.riders.push(id);
|
||||
rider_entry.insert(id);
|
||||
Ok(id)
|
||||
}
|
||||
} else {
|
||||
Err(MountingError::AlreadyRiding)
|
||||
}
|
||||
} else {
|
||||
Err(MountingError::MountDead)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn steer(
|
||||
&mut self,
|
||||
mount: NpcId,
|
||||
rider: impl Into<Actor>,
|
||||
) -> Result<MountId, MountingError> {
|
||||
self.add_mounting(mount, rider, true)
|
||||
}
|
||||
|
||||
pub fn ride(
|
||||
&mut self,
|
||||
mount: NpcId,
|
||||
rider: impl Into<Actor>,
|
||||
) -> Result<MountId, MountingError> {
|
||||
self.add_mounting(mount, rider, false)
|
||||
}
|
||||
|
||||
pub fn get_mount_link(&self, rider: impl Into<Actor>) -> Option<&NpcLink> {
|
||||
self.rider_map
|
||||
.get(&rider.into())
|
||||
.and_then(|link| self.links.get(*link))
|
||||
}
|
||||
|
||||
pub fn get_steerer_link(&self, mount: NpcId) -> Option<&NpcLink> {
|
||||
self.mount_map
|
||||
.get(mount)
|
||||
.and_then(|mount| self.links.get(mount.steerer?))
|
||||
}
|
||||
|
||||
pub fn get(&self, id: MountId) -> Option<&NpcLink> { self.links.get(id) }
|
||||
|
||||
pub fn ids(&self) -> impl Iterator<Item = MountId> + '_ { self.links.keys() }
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &NpcLink> + '_ { self.links.values() }
|
||||
|
||||
pub fn iter_mounts(&self) -> impl Iterator<Item = NpcId> + '_ {
|
||||
self.mount_map.keys()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HopSlotMap<MountId, NpcLink>> for NpcLinks {
|
||||
fn from(mut value: HopSlotMap<MountId, NpcLink>) -> Self {
|
||||
let mut from_map = slotmap::SecondaryMap::new();
|
||||
let mut to_map = HashMap::with_capacity(value.len());
|
||||
let mut delete = Vec::new();
|
||||
for (id, link) in value.iter() {
|
||||
if let Some(entry) = from_map.entry(link.mount) {
|
||||
let riders = entry.or_insert(Riders::default());
|
||||
if link.is_steering {
|
||||
if let Some(old) = riders.steerer.replace(id) {
|
||||
error!("Replaced steerer {old:?} with {id:?}");
|
||||
}
|
||||
} else {
|
||||
riders.riders.push(id);
|
||||
}
|
||||
} else {
|
||||
delete.push(id);
|
||||
}
|
||||
to_map.insert(link.rider, id);
|
||||
}
|
||||
for id in delete {
|
||||
value.remove(id);
|
||||
}
|
||||
Self {
|
||||
links: value,
|
||||
mount_map: from_map,
|
||||
rider_map: to_map,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<HopSlotMap<MountId, NpcLink>> for NpcLinks {
|
||||
fn into(self) -> HopSlotMap<MountId, NpcLink> { self.links }
|
||||
}
|
||||
slotmap::new_key_type! {
|
||||
pub struct MountId;
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct MountData {
|
||||
is_steering: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct Npcs {
|
||||
pub uid_counter: u64,
|
||||
pub npcs: HopSlotMap<NpcId, Npc>,
|
||||
pub vehicles: HopSlotMap<VehicleId, Vehicle>,
|
||||
pub mounts: NpcLinks,
|
||||
// TODO: This feels like it should be its own rtsim resource
|
||||
// TODO: Consider switching to `common::util::SpatialGrid` instead
|
||||
#[serde(skip, default = "construct_npc_grid")]
|
||||
@ -347,8 +485,9 @@ pub struct Npcs {
|
||||
impl Default for Npcs {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
uid_counter: 0,
|
||||
npcs: Default::default(),
|
||||
vehicles: Default::default(),
|
||||
mounts: Default::default(),
|
||||
npc_grid: construct_npc_grid(),
|
||||
character_map: Default::default(),
|
||||
}
|
||||
@ -357,11 +496,22 @@ impl Default for Npcs {
|
||||
|
||||
fn construct_npc_grid() -> Grid<GridCell> { Grid::new(Vec2::zero(), Default::default()) }
|
||||
|
||||
impl Npcs {
|
||||
pub fn create_npc(&mut self, npc: Npc) -> NpcId { self.npcs.insert(npc) }
|
||||
#[derive(Debug)]
|
||||
pub enum MountingError {
|
||||
MountDead,
|
||||
RiderDead,
|
||||
HasSteerer,
|
||||
AlreadyRiding,
|
||||
MountIsRiding,
|
||||
RiderIsMounted,
|
||||
MountSelf,
|
||||
}
|
||||
|
||||
pub fn create_vehicle(&mut self, vehicle: Vehicle) -> VehicleId {
|
||||
self.vehicles.insert(vehicle)
|
||||
impl Npcs {
|
||||
pub fn create_npc(&mut self, mut npc: Npc) -> NpcId {
|
||||
npc.uid = self.uid_counter;
|
||||
self.uid_counter += 1;
|
||||
self.npcs.insert(npc)
|
||||
}
|
||||
|
||||
/// Queries nearby npcs, not garantueed to work if radius > 32.0
|
||||
|
@ -2,7 +2,7 @@ use crate::{RtState, Rule};
|
||||
use common::{
|
||||
mounting::VolumePos,
|
||||
resources::{Time, TimeOfDay},
|
||||
rtsim::{Actor, VehicleId},
|
||||
rtsim::{Actor, NpcId},
|
||||
};
|
||||
use vek::*;
|
||||
use world::{IndexRef, World};
|
||||
@ -41,6 +41,6 @@ impl Event for OnDeath {}
|
||||
#[derive(Clone)]
|
||||
pub struct OnMountVolume {
|
||||
pub actor: Actor,
|
||||
pub pos: VolumePos<VehicleId>,
|
||||
pub pos: VolumePos<NpcId>,
|
||||
}
|
||||
impl Event for OnMountVolume {}
|
||||
|
@ -4,13 +4,12 @@ pub mod site;
|
||||
|
||||
use crate::data::{
|
||||
faction::Faction,
|
||||
npc::{Npc, Npcs, Profession, Vehicle},
|
||||
npc::{Npc, Npcs, Profession},
|
||||
site::Site,
|
||||
Data, Nature, CURRENT_VERSION,
|
||||
};
|
||||
use common::{
|
||||
comp::{self, Body},
|
||||
grid::Grid,
|
||||
resources::TimeOfDay,
|
||||
rtsim::{Personality, Role, WorldSettings},
|
||||
terrain::{BiomeKind, CoordinateConversions, TerrainChunkSize},
|
||||
@ -32,12 +31,7 @@ impl Data {
|
||||
let mut this = Self {
|
||||
version: CURRENT_VERSION,
|
||||
nature: Nature::generate(world),
|
||||
npcs: Npcs {
|
||||
npcs: Default::default(),
|
||||
vehicles: Default::default(),
|
||||
npc_grid: Grid::new(Vec2::zero(), Default::default()),
|
||||
character_map: Default::default(),
|
||||
},
|
||||
npcs: Npcs::default(),
|
||||
sites: Default::default(),
|
||||
factions: Default::default(),
|
||||
reports: Default::default(),
|
||||
@ -187,11 +181,14 @@ impl Data {
|
||||
|
||||
if rng.gen_bool(0.4) {
|
||||
let wpos = rand_wpos(&mut rng, matches_plazas) + Vec3::unit_z() * 50.0;
|
||||
let vehicle_id = this
|
||||
.npcs
|
||||
.create_vehicle(Vehicle::new(wpos, comp::body::ship::Body::DefaultAirship));
|
||||
let vehicle_id = this.npcs.create_npc(Npc::new(
|
||||
rng.gen(),
|
||||
wpos,
|
||||
Body::Ship(comp::body::ship::Body::DefaultAirship),
|
||||
Role::Vehicle,
|
||||
));
|
||||
|
||||
this.npcs.create_npc(
|
||||
let npc_id = this.npcs.create_npc(
|
||||
Npc::new(
|
||||
rng.gen(),
|
||||
wpos,
|
||||
@ -199,9 +196,12 @@ impl Data {
|
||||
Role::Civilised(Some(Profession::Captain)),
|
||||
)
|
||||
.with_home(site_id)
|
||||
.with_personality(Personality::random_good(&mut rng))
|
||||
.steering(vehicle_id),
|
||||
.with_personality(Personality::random_good(&mut rng)),
|
||||
);
|
||||
this.npcs
|
||||
.mounts
|
||||
.steer(vehicle_id, npc_id)
|
||||
.expect("We just created these npcs");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,7 @@ use crate::{
|
||||
use common::{
|
||||
astar::{Astar, PathResult},
|
||||
comp::{
|
||||
self,
|
||||
compass::{Direction, Distance},
|
||||
dialogue::Subject,
|
||||
Content,
|
||||
@ -252,7 +253,7 @@ impl Rule for NpcAi {
|
||||
data.npcs
|
||||
.iter_mut()
|
||||
// Don't run AI for dead NPCs
|
||||
.filter(|(_, npc)| !npc.is_dead)
|
||||
.filter(|(_, npc)| !npc.is_dead && !matches!(npc.role, Role::Vehicle))
|
||||
// Don't run AI for simulated NPCs every tick
|
||||
.filter(|(_, npc)| matches!(npc.mode, SimulationMode::Loaded) || (npc.seed as u64 + ctx.event.tick) % SIMULATED_TICK_SKIP == 0)
|
||||
.map(|(npc_id, npc)| {
|
||||
@ -1157,15 +1158,17 @@ fn react_to_events<S: State>(ctx: &mut NpcCtx, _: &mut S) -> Option<impl Action<
|
||||
|
||||
fn humanoid() -> impl Action<DefaultState> {
|
||||
choose(|ctx, _| {
|
||||
if let Some(riding) = &ctx.npc.riding {
|
||||
if riding.steering {
|
||||
if let Some(vehicle) = ctx.state.data().npcs.vehicles.get(riding.vehicle) {
|
||||
if let Some(riding) = &ctx.state.data().npcs.mounts.get_mount_link(ctx.npc_id) {
|
||||
if riding.is_steering {
|
||||
if let Some(vehicle) = ctx.state.data().npcs.get(riding.mount) {
|
||||
match vehicle.body {
|
||||
common::comp::ship::Body::DefaultAirship
|
||||
| common::comp::ship::Body::AirBalloon => important(pilot(vehicle.body)),
|
||||
common::comp::ship::Body::SailBoat | common::comp::ship::Body::Galleon => {
|
||||
important(captain())
|
||||
},
|
||||
comp::Body::Ship(
|
||||
body @ comp::ship::Body::DefaultAirship
|
||||
| body @ comp::ship::Body::AirBalloon,
|
||||
) => important(pilot(body)),
|
||||
comp::Body::Ship(
|
||||
comp::ship::Body::SailBoat | comp::ship::Body::Galleon,
|
||||
) => important(captain()),
|
||||
_ => casual(idle()),
|
||||
}
|
||||
} else {
|
||||
@ -1310,6 +1313,7 @@ fn think() -> impl Action<DefaultState> {
|
||||
.l(),
|
||||
Role::Monster => monster().r().r().l(),
|
||||
Role::Wild => idle().r(),
|
||||
Role::Vehicle => idle().r(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ use common::{
|
||||
};
|
||||
use rand::prelude::*;
|
||||
use rand_chacha::ChaChaRng;
|
||||
use slotmap::SecondaryMap;
|
||||
use tracing::{error, warn};
|
||||
use vek::{Clamp, Vec2};
|
||||
use world::{site::SiteKind, CONFIG};
|
||||
@ -29,29 +30,38 @@ impl Rule for SimulateNpcs {
|
||||
}
|
||||
}
|
||||
|
||||
fn on_setup(ctx: EventCtx<SimulateNpcs, OnSetup>) {
|
||||
fn on_setup(_ctx: EventCtx<SimulateNpcs, OnSetup>) {
|
||||
/*
|
||||
let data = &mut *ctx.state.data_mut();
|
||||
|
||||
// Add riders to vehicles
|
||||
for (npc_id, npc) in data.npcs.npcs.iter_mut() {
|
||||
if let Some(ride) = &npc.riding {
|
||||
if let Some(vehicle) = data.npcs.vehicles.get_mut(ride.vehicle) {
|
||||
let actor = Actor::Npc(npc_id);
|
||||
if ride.steering && vehicle.driver.replace(actor).is_some() {
|
||||
let riders = data
|
||||
.npcs
|
||||
.iter()
|
||||
.filter_map(|(id, npc)| Some((id, npc.relations.riding?)))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for (npc_id, ride) in riders {
|
||||
if let Some(mount) = data.npcs.get_mut(ride.npc) {
|
||||
if ride.steering && let Some(actor) = mount.relations.driver.replace(Actor::Npc(npc_id)) {
|
||||
error!("Replaced driver");
|
||||
npc.riding = None;
|
||||
if let Actor::Npc(npc) = actor && let Some(npc) = data.npcs.get_mut(npc) {
|
||||
npc.relations.riding = None;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
data.npcs[npc_id].relations.riding = None;
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
fn on_mount_volume(ctx: EventCtx<SimulateNpcs, OnMountVolume>) {
|
||||
let data = &mut *ctx.state.data_mut();
|
||||
|
||||
// TODO: Add actor to riders.
|
||||
if let VolumePos { kind: Volume::Entity(vehicle), .. } = ctx.event.pos
|
||||
&& let Some(vehicle) = data.npcs.vehicles.get(vehicle)
|
||||
&& let Some(Actor::Npc(driver)) = vehicle.driver
|
||||
&& let Some(link) = data.npcs.mounts.get_steerer_link(vehicle)
|
||||
&& let Actor::Npc(driver) = link.rider
|
||||
&& let Some(driver) = data.npcs.get_mut(driver) {
|
||||
driver.controller.actions.push(NpcAction::Say(Some(ctx.event.actor), comp::Content::localized("npc-speech-welcome-aboard")))
|
||||
}
|
||||
@ -71,8 +81,8 @@ fn on_death(ctx: EventCtx<SimulateNpcs, OnDeath>) {
|
||||
.sites
|
||||
.iter()
|
||||
.filter(|(id, site)| {
|
||||
// Don't respawn in the same town
|
||||
Some(*id) != npc.home
|
||||
&& (npc.faction.is_none() || site.faction == npc.faction)
|
||||
&& site.world_site.map_or(false, |s| {
|
||||
matches!(
|
||||
ctx.index.sites.get(s).kind,
|
||||
@ -181,76 +191,89 @@ fn on_death(ctx: EventCtx<SimulateNpcs, OnDeath>) {
|
||||
|
||||
fn on_tick(ctx: EventCtx<SimulateNpcs, OnTick>) {
|
||||
let data = &mut *ctx.state.data_mut();
|
||||
|
||||
// Maintain links
|
||||
let ids = data.npcs.mounts.ids().collect::<Vec<_>>();
|
||||
let mut mount_activity = SecondaryMap::new();
|
||||
for link_id in ids {
|
||||
if let Some(link) = data.npcs.mounts.get(link_id) {
|
||||
if let Some(mount) = data
|
||||
.npcs
|
||||
.npcs
|
||||
.get(link.mount)
|
||||
.filter(|mount| !mount.is_dead)
|
||||
{
|
||||
let wpos = mount.wpos;
|
||||
if let Actor::Npc(rider) = link.rider {
|
||||
if let Some(rider) =
|
||||
data.npcs.npcs.get_mut(rider).filter(|rider| !rider.is_dead)
|
||||
{
|
||||
rider.wpos = wpos;
|
||||
mount_activity.insert(link.mount, rider.controller.activity);
|
||||
} else {
|
||||
data.npcs.mounts.dismount(link.rider)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
data.npcs.mounts.remove_mount(link.mount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (npc_id, npc) in data.npcs.npcs.iter_mut().filter(|(_, npc)| !npc.is_dead) {
|
||||
if matches!(npc.mode, SimulationMode::Simulated) {
|
||||
// Simulate NPC movement when riding
|
||||
if let Some(riding) = &npc.riding {
|
||||
if let Some(vehicle) = data.npcs.vehicles.get_mut(riding.vehicle) {
|
||||
match npc.controller.activity {
|
||||
// If steering, the NPC controls the vehicle's motion
|
||||
Some(NpcActivity::Goto(target, speed_factor)) if riding.steering => {
|
||||
let diff = target - vehicle.wpos;
|
||||
let dist2 = diff.magnitude_squared();
|
||||
// Consume NPC actions
|
||||
for action in std::mem::take(&mut npc.controller.actions) {
|
||||
match action {
|
||||
NpcAction::Say(_, _) => {}, // Currently, just swallow interactions
|
||||
NpcAction::Attack(_) => {}, // TODO: Implement simulated combat
|
||||
}
|
||||
}
|
||||
|
||||
if dist2 > 0.5f32.powi(2) {
|
||||
let wpos = vehicle.wpos
|
||||
+ (diff
|
||||
* (vehicle.get_speed() * speed_factor * ctx.event.dt
|
||||
/ dist2.sqrt())
|
||||
.min(1.0));
|
||||
|
||||
let is_valid = match vehicle.body {
|
||||
common::comp::ship::Body::DefaultAirship
|
||||
| common::comp::ship::Body::AirBalloon => true,
|
||||
common::comp::ship::Body::SailBoat
|
||||
| common::comp::ship::Body::Galleon => {
|
||||
let chunk_pos = wpos.xy().as_().wpos_to_cpos();
|
||||
ctx.world
|
||||
.sim()
|
||||
.get(chunk_pos)
|
||||
.map_or(true, |f| f.river.river_kind.is_some())
|
||||
},
|
||||
_ => false,
|
||||
let activity = if data.npcs.mounts.get_mount_link(npc_id).is_some() {
|
||||
// We are riding, nothing to do.
|
||||
continue;
|
||||
} else if let Some(activity) = mount_activity.get(npc_id) {
|
||||
*activity
|
||||
} else {
|
||||
npc.controller.activity
|
||||
};
|
||||
|
||||
if is_valid {
|
||||
vehicle.wpos = wpos;
|
||||
}
|
||||
vehicle.dir = (target.xy() - vehicle.wpos.xy())
|
||||
.try_normalized()
|
||||
.unwrap_or(vehicle.dir);
|
||||
}
|
||||
},
|
||||
// When riding, other actions are disabled
|
||||
Some(
|
||||
NpcActivity::Goto(_, _)
|
||||
| NpcActivity::Gather(_)
|
||||
| NpcActivity::HuntAnimals
|
||||
| NpcActivity::Dance(_)
|
||||
| NpcActivity::Cheer(_)
|
||||
| NpcActivity::Sit(_),
|
||||
) => {},
|
||||
None => {},
|
||||
}
|
||||
npc.wpos = vehicle.wpos;
|
||||
} else {
|
||||
// Vehicle doens't exist anymore
|
||||
npc.riding = None;
|
||||
}
|
||||
// If not riding, we assume they're just walking
|
||||
} else {
|
||||
match npc.controller.activity {
|
||||
match activity {
|
||||
// Move NPCs if they have a target destination
|
||||
Some(NpcActivity::Goto(target, speed_factor)) => {
|
||||
let diff = target.xy() - npc.wpos.xy();
|
||||
let dist2 = diff.magnitude_squared();
|
||||
|
||||
if dist2 > 0.5f32.powi(2) {
|
||||
npc.wpos += (diff
|
||||
let new_wpos = npc.wpos
|
||||
+ (diff
|
||||
* (npc.body.max_speed_approx() * speed_factor * ctx.event.dt
|
||||
/ dist2.sqrt())
|
||||
.min(1.0))
|
||||
.with_z(0.0);
|
||||
|
||||
let is_valid = match npc.body {
|
||||
// Don't move water bound bodies outside of water.
|
||||
Body::Ship(comp::ship::Body::SailBoat | comp::ship::Body::Galleon)
|
||||
| Body::FishMedium(_)
|
||||
| Body::FishSmall(_) => {
|
||||
let chunk_pos = new_wpos.xy().as_().wpos_to_cpos();
|
||||
ctx.world
|
||||
.sim()
|
||||
.get(chunk_pos)
|
||||
.map_or(true, |f| f.river.river_kind.is_some())
|
||||
},
|
||||
_ => true,
|
||||
};
|
||||
|
||||
if is_valid {
|
||||
npc.wpos = new_wpos;
|
||||
}
|
||||
|
||||
npc.dir = (target.xy() - npc.wpos.xy())
|
||||
.try_normalized()
|
||||
.unwrap_or(npc.dir);
|
||||
}
|
||||
},
|
||||
Some(
|
||||
@ -265,28 +288,27 @@ fn on_tick(ctx: EventCtx<SimulateNpcs, OnTick>) {
|
||||
},
|
||||
None => {},
|
||||
}
|
||||
// Make sure NPCs remain on the surface and within the map
|
||||
npc.wpos = npc
|
||||
.wpos
|
||||
.xy()
|
||||
.clamped(
|
||||
|
||||
// Make sure NPCs remain in a valid location
|
||||
let clamped_wpos = npc.wpos.xy().clamped(
|
||||
Vec2::zero(),
|
||||
(ctx.world.sim().get_size() * TerrainChunkSize::RECT_SIZE).as_(),
|
||||
)
|
||||
.with_z(
|
||||
);
|
||||
match npc.body {
|
||||
Body::Ship(comp::ship::Body::DefaultAirship | comp::ship::Body::AirBalloon) => {
|
||||
npc.wpos = clamped_wpos.with_z(
|
||||
ctx.world
|
||||
.sim()
|
||||
.get_surface_alt_approx(npc.wpos.xy().map(|e| e as i32))
|
||||
.get_surface_alt_approx(clamped_wpos.as_())
|
||||
.max(npc.wpos.z),
|
||||
);
|
||||
},
|
||||
_ => {
|
||||
npc.wpos = clamped_wpos.with_z(
|
||||
ctx.world.sim().get_surface_alt_approx(clamped_wpos.as_())
|
||||
+ npc.body.flying_height(),
|
||||
);
|
||||
}
|
||||
|
||||
// Consume NPC actions
|
||||
for action in std::mem::take(&mut npc.controller.actions) {
|
||||
match action {
|
||||
NpcAction::Say(_, _) => {}, // Currently, just swallow interactions
|
||||
NpcAction::Attack(_) => {}, // TODO: Implement simulated combat
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -305,31 +327,4 @@ fn on_tick(ctx: EventCtx<SimulateNpcs, OnTick>) {
|
||||
npc.home = Some(new_home);
|
||||
}
|
||||
}
|
||||
|
||||
for (id, vehicle) in data.npcs.vehicles.iter_mut() {
|
||||
// Try to keep ships above ground and within the map
|
||||
if matches!(vehicle.mode, SimulationMode::Simulated) {
|
||||
vehicle.wpos = vehicle
|
||||
.wpos
|
||||
.xy()
|
||||
.clamped(
|
||||
Vec2::zero(),
|
||||
(ctx.world.sim().get_size() * TerrainChunkSize::RECT_SIZE).as_(),
|
||||
)
|
||||
.with_z(
|
||||
vehicle.wpos.z.max(
|
||||
ctx.world
|
||||
.sim()
|
||||
.get_surface_alt_approx(vehicle.wpos.xy().as_()),
|
||||
),
|
||||
);
|
||||
}
|
||||
if let Some(Actor::Npc(driver)) = vehicle.driver
|
||||
&& data.npcs.npcs.get(driver).and_then(|driver| {
|
||||
Some(driver.riding.as_ref()?.vehicle != id)
|
||||
}).unwrap_or(true)
|
||||
{
|
||||
vehicle.driver = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -84,24 +84,6 @@ fn on_death(ctx: EventCtx<SyncNpcs, OnDeath>) {
|
||||
|
||||
fn on_tick(ctx: EventCtx<SyncNpcs, OnTick>) {
|
||||
let data = &mut *ctx.state.data_mut();
|
||||
// Update vehicle grid cells
|
||||
for (vehicle_id, vehicle) in data.npcs.vehicles.iter_mut() {
|
||||
let chunk_pos = vehicle.wpos.xy().as_().wpos_to_cpos();
|
||||
if vehicle.chunk_pos != Some(chunk_pos) {
|
||||
if let Some(cell) = vehicle
|
||||
.chunk_pos
|
||||
.and_then(|chunk_pos| data.npcs.npc_grid.get_mut(chunk_pos))
|
||||
{
|
||||
if let Some(index) = cell.vehicles.iter().position(|id| *id == vehicle_id) {
|
||||
cell.vehicles.swap_remove(index);
|
||||
}
|
||||
}
|
||||
vehicle.chunk_pos = Some(chunk_pos);
|
||||
if let Some(cell) = data.npcs.npc_grid.get_mut(chunk_pos) {
|
||||
cell.vehicles.push(vehicle_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (npc_id, npc) in data.npcs.npcs.iter_mut() {
|
||||
// Update the NPC's current site, if any
|
||||
npc.current_site = ctx
|
||||
|
@ -674,7 +674,16 @@ fn handle_make_npc(
|
||||
} => {
|
||||
let mut entity_builder = server
|
||||
.state
|
||||
.create_npc(pos, stats, skill_set, health, poise, inventory, body)
|
||||
.create_npc(
|
||||
pos,
|
||||
comp::Ori::default(),
|
||||
stats,
|
||||
skill_set,
|
||||
health,
|
||||
poise,
|
||||
inventory,
|
||||
body,
|
||||
)
|
||||
.with(alignment)
|
||||
.with(scale)
|
||||
.with(comp::Vel(Vec3::new(0.0, 0.0, 0.0)));
|
||||
@ -1303,9 +1312,8 @@ fn handle_rtsim_tp(
|
||||
action: &ServerChatCommand,
|
||||
) -> CmdResult<()> {
|
||||
use crate::rtsim::RtSim;
|
||||
let (npc_index, dismount_volume) = parse_cmd_args!(args, u32, bool);
|
||||
let pos = if let Some(id) = npc_index {
|
||||
// TODO: Take some other identifier than an integer to this command.
|
||||
let (npc_id, dismount_volume) = parse_cmd_args!(args, u64, bool);
|
||||
let pos = if let Some(id) = npc_id {
|
||||
server
|
||||
.state
|
||||
.ecs()
|
||||
@ -1314,8 +1322,8 @@ fn handle_rtsim_tp(
|
||||
.data()
|
||||
.npcs
|
||||
.values()
|
||||
.nth(id as usize)
|
||||
.ok_or(action.help_string())?
|
||||
.find(|npc| npc.uid == id)
|
||||
.ok_or_else(|| format!("No NPC has the id {id}"))?
|
||||
.wpos
|
||||
} else {
|
||||
return Err(Content::Plain(action.help_string()));
|
||||
@ -1335,15 +1343,14 @@ fn handle_rtsim_info(
|
||||
action: &ServerChatCommand,
|
||||
) -> CmdResult<()> {
|
||||
use crate::rtsim::RtSim;
|
||||
if let Some(id) = parse_cmd_args!(args, u32) {
|
||||
// TODO: Take some other identifier than an integer to this command.
|
||||
if let Some(id) = parse_cmd_args!(args, u64) {
|
||||
let rtsim = server.state.ecs().read_resource::<RtSim>();
|
||||
let data = rtsim.state().data();
|
||||
let npc = data
|
||||
let (id, npc) = data
|
||||
.npcs
|
||||
.values()
|
||||
.nth(id as usize)
|
||||
.ok_or_else(|| format!("No NPC has index {}", id))?;
|
||||
.iter()
|
||||
.find(|(_, npc)| npc.uid == id)
|
||||
.ok_or_else(|| format!("No NPC has the id {id}"))?;
|
||||
|
||||
let mut info = String::new();
|
||||
|
||||
@ -1360,7 +1367,10 @@ fn handle_rtsim_info(
|
||||
let _ = writeln!(
|
||||
&mut info,
|
||||
"Riding: {:?}",
|
||||
npc.riding.as_ref().map(|riding| riding.vehicle)
|
||||
data.npcs
|
||||
.mounts
|
||||
.get_mount_link(id)
|
||||
.map(|link| data.npcs.get(link.mount).map_or(0, |mount| mount.uid))
|
||||
);
|
||||
let _ = writeln!(&mut info, "-- Action State --");
|
||||
if let Some(brain) = &npc.brain {
|
||||
@ -1404,8 +1414,7 @@ fn handle_rtsim_npc(
|
||||
let mut npcs = data
|
||||
.npcs
|
||||
.values()
|
||||
.enumerate()
|
||||
.filter(|(idx, npc)| {
|
||||
.filter(|npc| {
|
||||
let mut tags = vec![
|
||||
npc.profession()
|
||||
.map(|p| format!("{:?}", p))
|
||||
@ -1414,9 +1423,10 @@ fn handle_rtsim_npc(
|
||||
Role::Civilised(_) => "civilised".to_string(),
|
||||
Role::Wild => "wild".to_string(),
|
||||
Role::Monster => "monster".to_string(),
|
||||
Role::Vehicle => "vehicle".to_string(),
|
||||
},
|
||||
format!("{:?}", npc.mode),
|
||||
format!("{}", idx),
|
||||
format!("{}", npc.uid),
|
||||
npc_names[&npc.body].keyword.clone(),
|
||||
];
|
||||
if let Some(species_meta) = npc_names.get_species_meta(&npc.body) {
|
||||
@ -1428,14 +1438,14 @@ fn handle_rtsim_npc(
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if let Ok(pos) = position(server, target, "target") {
|
||||
npcs.sort_by_key(|(_, npc)| (npc.wpos.distance_squared(pos.0) * 10.0) as u64);
|
||||
npcs.sort_by_key(|npc| (npc.wpos.distance_squared(pos.0) * 10.0) as u64);
|
||||
}
|
||||
|
||||
let mut info = String::new();
|
||||
|
||||
let _ = writeln!(&mut info, "-- NPCs matching [{}] --", terms.join(", "));
|
||||
for (idx, npc) in npcs.iter().take(count.unwrap_or(!0) as usize) {
|
||||
let _ = write!(&mut info, "{} ({}), ", npc.get_name(), idx);
|
||||
for npc in npcs.iter().take(count.unwrap_or(!0) as usize) {
|
||||
let _ = write!(&mut info, "{} ({}), ", npc.get_name(), npc.uid);
|
||||
}
|
||||
let _ = writeln!(&mut info);
|
||||
let _ = writeln!(
|
||||
@ -1600,6 +1610,7 @@ fn handle_spawn(
|
||||
.state
|
||||
.create_npc(
|
||||
pos,
|
||||
comp::Ori::default(),
|
||||
comp::Stats::new(get_npc_name(id, npc::BodyType::from_body(body)), body),
|
||||
comp::SkillSet::default(),
|
||||
Some(comp::Health::new(body, 0)),
|
||||
@ -1706,6 +1717,7 @@ fn handle_spawn_training_dummy(
|
||||
.state
|
||||
.create_npc(
|
||||
pos,
|
||||
comp::Ori::default(),
|
||||
stats,
|
||||
skill_set,
|
||||
Some(health),
|
||||
|
@ -17,7 +17,7 @@ use common::{
|
||||
mounting::{Mounting, Volume, VolumeMounting, VolumePos},
|
||||
outcome::Outcome,
|
||||
resources::{Secs, Time},
|
||||
rtsim::RtSimVehicle,
|
||||
rtsim::RtSimEntity,
|
||||
uid::{IdMaps, Uid},
|
||||
util::Dir,
|
||||
vol::IntoFullVolIterator,
|
||||
@ -99,11 +99,18 @@ pub fn handle_loaded_character_data(
|
||||
server.notify_client(entity, result_msg);
|
||||
}
|
||||
|
||||
pub fn handle_create_npc(server: &mut Server, pos: Pos, mut npc: NpcBuilder) -> EcsEntity {
|
||||
pub fn handle_create_npc(
|
||||
server: &mut Server,
|
||||
pos: Pos,
|
||||
ori: Ori,
|
||||
mut npc: NpcBuilder,
|
||||
rider: Option<NpcBuilder>,
|
||||
) -> EcsEntity {
|
||||
let entity = server
|
||||
.state
|
||||
.create_npc(
|
||||
pos,
|
||||
ori,
|
||||
npc.stats,
|
||||
npc.skill_set,
|
||||
npc.health,
|
||||
@ -168,8 +175,8 @@ pub fn handle_create_npc(server: &mut Server, pos: Pos, mut npc: NpcBuilder) ->
|
||||
// Add to group system if a pet
|
||||
if let comp::Alignment::Owned(owner_uid) = npc.alignment {
|
||||
let state = server.state();
|
||||
let clients = state.ecs().read_storage::<Client>();
|
||||
let uids = state.ecs().read_storage::<Uid>();
|
||||
let clients = state.ecs().read_storage::<Client>();
|
||||
let mut group_manager = state.ecs().write_resource::<comp::group::GroupManager>();
|
||||
if let Some(owner) = state.ecs().entity_from_uid(owner_uid) {
|
||||
let map_markers = state.ecs().read_storage::<comp::MapMarker>();
|
||||
@ -207,6 +214,20 @@ pub fn handle_create_npc(server: &mut Server, pos: Pos, mut npc: NpcBuilder) ->
|
||||
let _ = server.state.ecs().write_storage().insert(new_entity, group);
|
||||
}
|
||||
|
||||
if let Some(rider) = rider {
|
||||
let rider_entity = handle_create_npc(server, pos, Ori::default(), rider, None);
|
||||
let uids = server.state().ecs().read_storage::<Uid>();
|
||||
let link = Mounting {
|
||||
mount: *uids.get(new_entity).expect("We just created this entity"),
|
||||
rider: *uids.get(rider_entity).expect("We just created this entity"),
|
||||
};
|
||||
drop(uids);
|
||||
server
|
||||
.state
|
||||
.link(link)
|
||||
.expect("We just created these entities");
|
||||
}
|
||||
|
||||
new_entity
|
||||
}
|
||||
|
||||
@ -215,7 +236,7 @@ pub fn handle_create_ship(
|
||||
pos: Pos,
|
||||
ori: Ori,
|
||||
ship: comp::ship::Body,
|
||||
rtsim_vehicle: Option<RtSimVehicle>,
|
||||
rtsim_entity: Option<RtSimEntity>,
|
||||
driver: Option<NpcBuilder>,
|
||||
passengers: Vec<NpcBuilder>,
|
||||
) {
|
||||
@ -252,13 +273,13 @@ pub fn handle_create_ship(
|
||||
entity = entity.with(agent);
|
||||
}
|
||||
*/
|
||||
if let Some(rtsim_vehicle) = rtsim_vehicle {
|
||||
if let Some(rtsim_vehicle) = rtsim_entity {
|
||||
entity = entity.with(rtsim_vehicle);
|
||||
}
|
||||
let entity = entity.build();
|
||||
|
||||
if let Some(driver) = driver {
|
||||
let npc_entity = handle_create_npc(server, pos, driver);
|
||||
let npc_entity = handle_create_npc(server, pos, ori, driver, None);
|
||||
|
||||
let uids = server.state.ecs().read_storage::<Uid>();
|
||||
let (rider_uid, mount_uid) = uids
|
||||
@ -292,7 +313,13 @@ pub fn handle_create_ship(
|
||||
}
|
||||
|
||||
for passenger in passengers {
|
||||
let npc_entity = handle_create_npc(server, Pos(pos.0 + Vec3::unit_z() * 5.0), passenger);
|
||||
let npc_entity = handle_create_npc(
|
||||
server,
|
||||
Pos(pos.0 + Vec3::unit_z() * 5.0),
|
||||
ori,
|
||||
passenger,
|
||||
None,
|
||||
);
|
||||
if let Some((rider_pos, rider_block)) = seats.next() {
|
||||
let uids = server.state.ecs().read_storage::<Uid>();
|
||||
let (rider_uid, mount_uid) = uids
|
||||
|
@ -22,7 +22,7 @@ use common::{
|
||||
link::Is,
|
||||
mounting::{Mount, Mounting, Rider, VolumeMounting, VolumePos, VolumeRider},
|
||||
outcome::Outcome,
|
||||
rtsim::RtSimVehicle,
|
||||
rtsim::RtSimEntity,
|
||||
terrain::{Block, SpriteKind},
|
||||
uid::{IdMaps, Uid},
|
||||
vol::ReadVol,
|
||||
@ -201,7 +201,7 @@ pub fn handle_mount_volume(server: &mut Server, rider: EcsEntity, volume_pos: Vo
|
||||
&& let Some(rider_actor) = state.entity_as_actor(rider_entity)
|
||||
&& let Some(volume_pos) = volume_pos.try_map_entity(|uid| {
|
||||
let entity = uid_allocator.uid_entity(uid)?;
|
||||
state.read_storage::<RtSimVehicle>().get(entity).map(|v| v.0)
|
||||
state.read_storage::<RtSimEntity>().get(entity).map(|v| v.0)
|
||||
}) {
|
||||
state.ecs().write_resource::<RtSim>().hook_character_mount_volume(
|
||||
&state.ecs().read_resource::<Arc<world::World>>(),
|
||||
|
@ -196,8 +196,13 @@ impl Server {
|
||||
ServerEvent::ExitIngame { entity } => {
|
||||
handle_exit_ingame(self, entity, false);
|
||||
},
|
||||
ServerEvent::CreateNpc { pos, npc } => {
|
||||
handle_create_npc(self, pos, npc);
|
||||
ServerEvent::CreateNpc {
|
||||
pos,
|
||||
ori,
|
||||
npc,
|
||||
rider,
|
||||
} => {
|
||||
handle_create_npc(self, pos, ori, npc, rider);
|
||||
},
|
||||
ServerEvent::CreateShip {
|
||||
pos,
|
||||
|
@ -74,7 +74,7 @@ use common::{
|
||||
event::{EventBus, ServerEvent},
|
||||
region::RegionMap,
|
||||
resources::{BattleMode, GameMode, Time, TimeOfDay},
|
||||
rtsim::{RtSimEntity, RtSimVehicle},
|
||||
rtsim::RtSimEntity,
|
||||
shared_server_config::ServerConstants,
|
||||
slowjob::SlowJobPool,
|
||||
terrain::{TerrainChunk, TerrainChunkSize},
|
||||
@ -405,7 +405,6 @@ impl Server {
|
||||
state.ecs_mut().register::<login_provider::PendingLogin>();
|
||||
state.ecs_mut().register::<RepositionOnChunkLoad>();
|
||||
state.ecs_mut().register::<RtSimEntity>();
|
||||
state.ecs_mut().register::<RtSimVehicle>();
|
||||
|
||||
// Load banned words list
|
||||
let banned_words = settings.moderation.load_banned_words(data_dir);
|
||||
@ -846,7 +845,6 @@ impl Server {
|
||||
}
|
||||
|
||||
let mut rtsim = self.state.ecs().write_resource::<rtsim::RtSim>();
|
||||
let rtsim_entities = self.state.ecs().read_storage::<RtSimEntity>();
|
||||
// Remove NPCs that are outside the view distances of all players
|
||||
// This is done by removing NPCs in unloaded chunks
|
||||
let to_delete = {
|
||||
@ -856,15 +854,9 @@ impl Server {
|
||||
&self.state.ecs().read_storage::<comp::Pos>(),
|
||||
!&self.state.ecs().read_storage::<comp::Presence>(),
|
||||
self.state.ecs().read_storage::<Anchor>().maybe(),
|
||||
rtsim_entities.maybe(),
|
||||
)
|
||||
.join()
|
||||
.filter(|(_, pos, _, anchor, rtsim_entity)| {
|
||||
if rtsim_entity.map_or(false, |rtsim_entity| {
|
||||
!rtsim.can_unload_entity(*rtsim_entity)
|
||||
}) {
|
||||
return false;
|
||||
}
|
||||
.filter(|(_, pos, _, anchor)| {
|
||||
let chunk_key = terrain.pos_key(pos.0.map(|e| e.floor() as i32));
|
||||
match anchor {
|
||||
Some(Anchor::Chunk(hc)) => {
|
||||
@ -879,26 +871,20 @@ impl Server {
|
||||
None => terrain.get_key_real(chunk_key).is_none(),
|
||||
}
|
||||
})
|
||||
.map(|(entity, _, _, _, _)| entity)
|
||||
.map(|(entity, _, _, _)| entity)
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
#[cfg(feature = "worldgen")]
|
||||
{
|
||||
let rtsim_vehicles = self.state.ecs().read_storage::<RtSimVehicle>();
|
||||
|
||||
let rtsim_entities = self.state.ecs().read_storage::<RtSimEntity>();
|
||||
// Assimilate entities that are part of the real-time world simulation
|
||||
for entity in &to_delete {
|
||||
#[cfg(feature = "worldgen")]
|
||||
if let Some(rtsim_entity) = rtsim_entities.get(*entity) {
|
||||
rtsim.hook_rtsim_entity_unload(*rtsim_entity);
|
||||
}
|
||||
#[cfg(feature = "worldgen")]
|
||||
if let Some(rtsim_vehicle) = rtsim_vehicles.get(*entity) {
|
||||
rtsim.hook_rtsim_vehicle_unload(*rtsim_vehicle);
|
||||
}
|
||||
}
|
||||
}
|
||||
drop(rtsim_entities);
|
||||
drop(rtsim);
|
||||
|
||||
// Actually perform entity deletion
|
||||
|
@ -6,7 +6,7 @@ use atomicwrites::{AtomicFile, OverwriteBehavior};
|
||||
use common::{
|
||||
grid::Grid,
|
||||
mounting::VolumePos,
|
||||
rtsim::{Actor, ChunkResource, RtSimEntity, RtSimVehicle, VehicleId, WorldSettings},
|
||||
rtsim::{Actor, ChunkResource, NpcId, RtSimEntity, WorldSettings},
|
||||
};
|
||||
use common_ecs::{dispatch, System};
|
||||
use common_state::BlockDiff;
|
||||
@ -151,7 +151,7 @@ impl RtSim {
|
||||
&mut self,
|
||||
world: &World,
|
||||
index: IndexRef,
|
||||
pos: VolumePos<VehicleId>,
|
||||
pos: VolumePos<NpcId>,
|
||||
actor: Actor,
|
||||
) {
|
||||
self.state.emit(OnMountVolume { actor, pos }, world, index)
|
||||
@ -177,31 +177,13 @@ impl RtSim {
|
||||
}
|
||||
|
||||
pub fn hook_rtsim_entity_unload(&mut self, entity: RtSimEntity) {
|
||||
if let Some(npc) = self.state.get_data_mut().npcs.get_mut(entity.0) {
|
||||
npc.mode = SimulationMode::Simulated;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn can_unload_entity(&self, entity: RtSimEntity) -> bool {
|
||||
let data = self.state.data();
|
||||
data.npcs
|
||||
.get(entity.0)
|
||||
.and_then(|npc| {
|
||||
let riding = npc.riding.as_ref()?;
|
||||
let vehicle = data.npcs.vehicles.get(riding.vehicle)?;
|
||||
Some(matches!(vehicle.mode, SimulationMode::Simulated))
|
||||
})
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
pub fn hook_rtsim_vehicle_unload(&mut self, entity: RtSimVehicle) {
|
||||
let data = self.state.get_data_mut();
|
||||
if let Some(vehicle) = data.npcs.vehicles.get_mut(entity.0) {
|
||||
vehicle.mode = SimulationMode::Simulated;
|
||||
if let Some(Actor::Npc(npc)) = vehicle.driver {
|
||||
if let Some(npc) = data.npcs.get_mut(npc) {
|
||||
if data.npcs.mounts.get_mount_link(entity.0).is_none() {
|
||||
if let Some(npc) = data.npcs.get_mut(entity.0) {
|
||||
npc.mode = SimulationMode::Simulated;
|
||||
}
|
||||
if let Some(steerer) = data.npcs.mounts.get_steerer_link(entity.0) && let Actor::Npc(npc) = steerer.rider && let Some(npc) = data.npcs.get_mut(npc) {
|
||||
npc.mode = SimulationMode::Simulated;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ use common::{
|
||||
event::{EventBus, NpcBuilder, ServerEvent},
|
||||
generation::{BodyBuilder, EntityConfig, EntityInfo},
|
||||
resources::{DeltaTime, Time, TimeOfDay},
|
||||
rtsim::{Actor, RtSimEntity, RtSimVehicle},
|
||||
rtsim::{Actor, NpcId, RtSimEntity},
|
||||
slowjob::SlowJobPool,
|
||||
terrain::CoordinateConversions,
|
||||
trade::{Good, SiteInformation},
|
||||
@ -223,7 +223,6 @@ impl<'a> System<'a> for Sys {
|
||||
ReadExpect<'a, SlowJobPool>,
|
||||
ReadStorage<'a, comp::Pos>,
|
||||
ReadStorage<'a, RtSimEntity>,
|
||||
ReadStorage<'a, RtSimVehicle>,
|
||||
WriteStorage<'a, comp::Agent>,
|
||||
ReadStorage<'a, Presence>,
|
||||
);
|
||||
@ -246,7 +245,6 @@ impl<'a> System<'a> for Sys {
|
||||
slow_jobs,
|
||||
positions,
|
||||
rtsim_entities,
|
||||
rtsim_vehicles,
|
||||
mut agents,
|
||||
presences,
|
||||
): Self::SystemData,
|
||||
@ -294,14 +292,67 @@ impl<'a> System<'a> for Sys {
|
||||
let chunk_states = rtsim.state.resource::<ChunkStates>();
|
||||
let data = &mut *rtsim.state.data_mut();
|
||||
|
||||
// Load in vehicles
|
||||
for (vehicle_id, vehicle) in data.npcs.vehicles.iter_mut() {
|
||||
let chunk = vehicle.wpos.xy().as_::<i32>().wpos_to_cpos();
|
||||
let mut create_event = |id: NpcId, npc: &Npc, steering: Option<NpcBuilder>| match npc.body {
|
||||
Body::Ship(body) => {
|
||||
emitter.emit(ServerEvent::CreateShip {
|
||||
pos: comp::Pos(npc.wpos),
|
||||
ori: comp::Ori::from(Dir::new(npc.dir.with_z(0.0))),
|
||||
ship: body,
|
||||
rtsim_entity: Some(RtSimEntity(id)),
|
||||
driver: steering,
|
||||
});
|
||||
},
|
||||
_ => {
|
||||
let entity_info = get_npc_entity_info(npc, &data.sites, index.as_index_ref());
|
||||
|
||||
if matches!(vehicle.mode, SimulationMode::Simulated)
|
||||
emitter.emit(match NpcData::from_entity_info(entity_info) {
|
||||
NpcData::Data {
|
||||
pos,
|
||||
stats,
|
||||
skill_set,
|
||||
health,
|
||||
poise,
|
||||
inventory,
|
||||
agent,
|
||||
body,
|
||||
alignment,
|
||||
scale,
|
||||
loot,
|
||||
} => ServerEvent::CreateNpc {
|
||||
pos,
|
||||
ori: comp::Ori::from(Dir::new(npc.dir.with_z(0.0))),
|
||||
npc: NpcBuilder::new(stats, body, alignment)
|
||||
.with_skill_set(skill_set)
|
||||
.with_health(health)
|
||||
.with_poise(poise)
|
||||
.with_inventory(inventory)
|
||||
.with_agent(agent.map(|agent| Agent {
|
||||
rtsim_outbox: Some(Default::default()),
|
||||
..agent
|
||||
}))
|
||||
.with_scale(scale)
|
||||
.with_loot(loot)
|
||||
.with_rtsim(RtSimEntity(id)),
|
||||
rider: steering,
|
||||
},
|
||||
// EntityConfig can't represent Waypoints at all
|
||||
// as of now, and if someone will try to spawn
|
||||
// rtsim waypoint it is definitely error.
|
||||
NpcData::Waypoint(_) => unimplemented!(),
|
||||
NpcData::Teleporter(_, _) => unimplemented!(),
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
// Load in mounted npcs and their riders
|
||||
for mount in data.npcs.mounts.iter_mounts() {
|
||||
let mount_npc = data.npcs.npcs.get_mut(mount).expect("This should exist");
|
||||
let chunk = mount_npc.wpos.xy().as_::<i32>().wpos_to_cpos();
|
||||
|
||||
if matches!(mount_npc.mode, SimulationMode::Simulated)
|
||||
&& chunk_states.0.get(chunk).map_or(false, |c| c.is_some())
|
||||
{
|
||||
vehicle.mode = SimulationMode::Loaded;
|
||||
mount_npc.mode = SimulationMode::Loaded;
|
||||
|
||||
let mut actor_info = |actor: Actor| {
|
||||
let npc_id = actor.npc()?;
|
||||
@ -348,13 +399,14 @@ impl<'a> System<'a> for Sys {
|
||||
}
|
||||
};
|
||||
|
||||
emitter.emit(ServerEvent::CreateShip {
|
||||
pos: comp::Pos(vehicle.wpos),
|
||||
ori: comp::Ori::from(Dir::new(vehicle.dir.with_z(0.0))),
|
||||
ship: vehicle.body,
|
||||
rtsim_entity: Some(RtSimVehicle(vehicle_id)),
|
||||
driver: vehicle.driver.and_then(&mut actor_info),
|
||||
});
|
||||
let steerer = data
|
||||
.npcs
|
||||
.mounts
|
||||
.get_steerer_link(mount)
|
||||
.and_then(|link| actor_info(link.rider));
|
||||
|
||||
let mount_npc = data.npcs.npcs.get(mount).expect("This should exist");
|
||||
create_event(mount, mount_npc, steerer);
|
||||
}
|
||||
}
|
||||
|
||||
@ -367,60 +419,13 @@ impl<'a> System<'a> for Sys {
|
||||
if matches!(npc.mode, SimulationMode::Simulated)
|
||||
&& chunk_states.0.get(chunk).map_or(false, |c| c.is_some())
|
||||
// Riding npcs will be spawned by the vehicle.
|
||||
&& npc.riding.is_none()
|
||||
&& data.npcs.mounts.get_mount_link(npc_id).is_none()
|
||||
{
|
||||
npc.mode = SimulationMode::Loaded;
|
||||
let entity_info = get_npc_entity_info(npc, &data.sites, index.as_index_ref());
|
||||
|
||||
emitter.emit(match NpcData::from_entity_info(entity_info) {
|
||||
NpcData::Data {
|
||||
pos,
|
||||
stats,
|
||||
skill_set,
|
||||
health,
|
||||
poise,
|
||||
inventory,
|
||||
agent,
|
||||
body,
|
||||
alignment,
|
||||
scale,
|
||||
loot,
|
||||
} => ServerEvent::CreateNpc {
|
||||
pos,
|
||||
npc: NpcBuilder::new(stats, body, alignment)
|
||||
.with_skill_set(skill_set)
|
||||
.with_health(health)
|
||||
.with_poise(poise)
|
||||
.with_inventory(inventory)
|
||||
.with_agent(agent.map(|agent| Agent {
|
||||
rtsim_outbox: Some(Default::default()),
|
||||
..agent
|
||||
}))
|
||||
.with_scale(scale)
|
||||
.with_loot(loot)
|
||||
.with_rtsim(RtSimEntity(npc_id)),
|
||||
},
|
||||
// EntityConfig can't represent Waypoints at all
|
||||
// as of now, and if someone will try to spawn
|
||||
// rtsim waypoint it is definitely error.
|
||||
NpcData::Waypoint(_) => unimplemented!(),
|
||||
NpcData::Teleporter(_, _) => unimplemented!(),
|
||||
});
|
||||
create_event(npc_id, npc, None);
|
||||
}
|
||||
}
|
||||
|
||||
// Synchronise rtsim NPC with entity data
|
||||
for (pos, rtsim_vehicle) in (&positions, &rtsim_vehicles).join() {
|
||||
data.npcs
|
||||
.vehicles
|
||||
.get_mut(rtsim_vehicle.0)
|
||||
.filter(|npc| matches!(npc.mode, SimulationMode::Loaded))
|
||||
.map(|vehicle| {
|
||||
// Update rtsim NPC state
|
||||
vehicle.wpos = pos.0;
|
||||
});
|
||||
}
|
||||
|
||||
let mut emitter = server_event_bus.emitter();
|
||||
// Synchronise rtsim NPC with entity data
|
||||
for (entity, pos, rtsim_entity, agent) in (
|
||||
|
@ -52,6 +52,7 @@ pub trait StateExt {
|
||||
fn create_npc(
|
||||
&mut self,
|
||||
pos: comp::Pos,
|
||||
ori: comp::Ori,
|
||||
stats: comp::Stats,
|
||||
skill_set: comp::SkillSet,
|
||||
health: Option<comp::Health>,
|
||||
@ -275,6 +276,7 @@ impl StateExt for State {
|
||||
fn create_npc(
|
||||
&mut self,
|
||||
pos: comp::Pos,
|
||||
ori: comp::Ori,
|
||||
stats: comp::Stats,
|
||||
skill_set: comp::SkillSet,
|
||||
health: Option<comp::Health>,
|
||||
@ -286,14 +288,7 @@ impl StateExt for State {
|
||||
.create_entity_synced()
|
||||
.with(pos)
|
||||
.with(comp::Vel(Vec3::zero()))
|
||||
.with(
|
||||
comp::Ori::from_unnormalized_vec(Vec3::new(
|
||||
thread_rng().gen_range(-1.0..1.0),
|
||||
thread_rng().gen_range(-1.0..1.0),
|
||||
0.0,
|
||||
))
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.with(ori)
|
||||
.with(body.mass())
|
||||
.with(body.density())
|
||||
.with(body.collider())
|
||||
@ -787,10 +782,20 @@ impl StateExt for State {
|
||||
// This is the same as wild creatures naturally spawned in the world
|
||||
const DEFAULT_PET_HEALTH_LEVEL: u16 = 0;
|
||||
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
for (pet, body, stats) in pets {
|
||||
let ori = common::util::Dir::from_unnormalized(Vec3::new(
|
||||
rng.gen_range(-1.0..=1.0),
|
||||
rng.gen_range(-1.0..=1.0),
|
||||
0.0,
|
||||
))
|
||||
.map(|dir| comp::Ori::from(dir))
|
||||
.unwrap_or_default();
|
||||
let pet_entity = self
|
||||
.create_npc(
|
||||
player_pos,
|
||||
ori,
|
||||
stats,
|
||||
comp::SkillSet::default(),
|
||||
Some(comp::Health::new(body, DEFAULT_PET_HEALTH_LEVEL)),
|
||||
|
@ -2,6 +2,7 @@
|
||||
use crate::test_world::{IndexOwned, World};
|
||||
#[cfg(feature = "persistent_world")]
|
||||
use crate::TerrainPersistence;
|
||||
use rand::Rng;
|
||||
#[cfg(feature = "worldgen")]
|
||||
use world::{IndexOwned, World};
|
||||
|
||||
@ -137,6 +138,7 @@ impl<'a> System<'a> for Sys {
|
||||
)
|
||||
});
|
||||
|
||||
let mut rng = rand::thread_rng();
|
||||
// Fetch any generated `TerrainChunk`s and insert them into the terrain.
|
||||
// Also, send the chunk data to anybody that is close by.
|
||||
let mut new_chunks = Vec::new();
|
||||
@ -211,6 +213,13 @@ impl<'a> System<'a> for Sys {
|
||||
} => {
|
||||
server_emitter.emit(ServerEvent::CreateNpc {
|
||||
pos,
|
||||
ori: common::util::Dir::from_unnormalized(Vec3::new(
|
||||
rng.gen_range(-1.0..=1.0),
|
||||
rng.gen_range(-1.0..=1.0),
|
||||
0.0,
|
||||
))
|
||||
.map(|dir| comp::Ori::from(dir))
|
||||
.unwrap_or_default(),
|
||||
npc: NpcBuilder::new(stats, body, alignment)
|
||||
.with_skill_set(skill_set)
|
||||
.with_health(health)
|
||||
@ -220,6 +229,7 @@ impl<'a> System<'a> for Sys {
|
||||
.with_scale(scale)
|
||||
.with_anchor(comp::Anchor::Chunk(key))
|
||||
.with_loot(loot),
|
||||
rider: None,
|
||||
});
|
||||
},
|
||||
NpcData::Teleporter(pos, teleporter) => {
|
||||
|
Loading…
Reference in New Issue
Block a user