Make vehicles npcs and npc uid used in rtsim commands

This commit is contained in:
Isse 2023-10-03 14:23:47 +02:00
parent b9bfb3f1dd
commit 1f195fd848
19 changed files with 606 additions and 431 deletions

View File

@ -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>),

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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

View File

@ -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 {}

View File

@ -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");
}
}

View File

@ -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(),
},
})
}

View File

@ -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;
}
}
}

View File

@ -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

View File

@ -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),

View File

@ -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

View File

@ -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>>(),

View File

@ -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,

View File

@ -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

View File

@ -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;
}
}
}

View File

@ -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 (

View File

@ -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)),

View File

@ -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) => {