rtsim vehicles

This commit is contained in:
Isse
2023-01-14 23:49:43 +01:00
committed by Joshua Barretto
parent a7588e274d
commit 1a117f1331
22 changed files with 843 additions and 246 deletions

View File

@ -0,0 +1,19 @@
#![enable(implicit_some)]
(
name: Name("Captain"),
body: RandomWith("humanoid"),
alignment: Alignment(Npc),
loot: LootTable("common.loot_tables.creature.humanoid"),
inventory: (
loadout: Inline((
inherit: Asset("common.loadout.village.captain"),
active_hands: InHands((ModularWeapon(tool: Sword, material: Orichalcum, hands: Two), None)),
)),
items: [
(10, "common.items.food.cheese"),
(10, "common.items.food.plainsalad"),
(10, "common.items.consumable.potion_med"),
],
),
meta: [],
)

View File

@ -0,0 +1,26 @@
// Christmas event
//(1.0, Some(Item("common.items.calendar.christmas.armor.misc.head.woolly_wintercap"))),
#![enable(implicit_some)]
(
head: Item("common.items.armor.pirate.hat"),
shoulders: Item("common.items.armor.mail.orichalcum.shoulder"),
chest: Item("common.items.armor.mail.orichalcum.chest"),
gloves: Item("common.items.armor.mail.orichalcum.hand"),
back: Choice([
(1, Item("common.items.armor.misc.back.backpack")),
(1, Item("common.items.npc_armor.back.backpack_blue")),
(1, Item("common.items.armor.mail.orichalcum.back")),
(1, None),
]),
belt: Item("common.items.armor.mail.orichalcum.belt"),
legs: Item("common.items.armor.mail.orichalcum.pants"),
feet: Item("common.items.armor.mail.orichalcum.foot"),
lantern: Choice([
(1, Item("common.items.lantern.black_0")),
(1, Item("common.items.lantern.blue_0")),
(1, Item("common.items.lantern.green_0")),
(1, Item("common.items.lantern.red_0")),
(1, Item("common.items.lantern.geode_purp")),
(1, Item("common.items.boss_drops.lantern")),
]),
)

View File

@ -904,21 +904,20 @@ impl<F: Fn(Vec3<f32>, Vec3<f32>) -> f32, const NUM_SAMPLES: usize> PidController
/// Get the PID coefficients associated with some Body, since it will likely /// Get the PID coefficients associated with some Body, since it will likely
/// need to be tuned differently for each body type /// need to be tuned differently for each body type
pub fn pid_coefficients(body: &Body) -> (f32, f32, f32) { pub fn pid_coefficients(body: &Body) -> Option<(f32, f32, f32)> {
match body { match body {
Body::Ship(ship::Body::DefaultAirship) => { Body::Ship(ship::Body::DefaultAirship) => {
let kp = 1.0; let kp = 1.0;
let ki = 0.1; let ki = 0.1;
let kd = 1.2; let kd = 1.2;
(kp, ki, kd) Some((kp, ki, kd))
}, },
Body::Ship(ship::Body::AirBalloon) => { Body::Ship(ship::Body::AirBalloon) => {
let kp = 1.0; let kp = 1.0;
let ki = 0.1; let ki = 0.1;
let kd = 0.8; let kd = 0.8;
(kp, ki, kd) Some((kp, ki, kd))
}, },
// default to a pure-proportional controller, which is the first step when tuning _ => None,
_ => (1.0, 0.0, 0.0),
} }
} }

View File

@ -8,7 +8,7 @@ use crate::{
}, },
lottery::LootSpec, lottery::LootSpec,
outcome::Outcome, outcome::Outcome,
rtsim::RtSimEntity, rtsim::{RtSimEntity, RtSimVehicle},
terrain::SpriteKind, terrain::SpriteKind,
trade::{TradeAction, TradeId}, trade::{TradeAction, TradeId},
uid::Uid, uid::Uid,
@ -42,6 +42,83 @@ pub struct UpdateCharacterMetadata {
pub skill_set_persistence_load_error: Option<comp::skillset::SkillsPersistenceError>, pub skill_set_persistence_load_error: Option<comp::skillset::SkillsPersistenceError>,
} }
pub struct NpcBuilder {
pub stats: comp::Stats,
pub skill_set: comp::SkillSet,
pub health: Option<comp::Health>,
pub poise: comp::Poise,
pub inventory: comp::inventory::Inventory,
pub body: comp::Body,
pub agent: Option<comp::Agent>,
pub alignment: comp::Alignment,
pub scale: comp::Scale,
pub anchor: Option<comp::Anchor>,
pub loot: LootSpec<String>,
pub rtsim_entity: Option<RtSimEntity>,
pub projectile: Option<comp::Projectile>,
}
impl NpcBuilder {
pub fn new(stats: comp::Stats, body: comp::Body, alignment: comp::Alignment) -> Self {
Self {
stats,
skill_set: comp::SkillSet::default(),
health: None,
poise: comp::Poise::new(body.clone()),
inventory: comp::Inventory::with_empty(),
body,
agent: None,
alignment,
scale: comp::Scale(1.0),
anchor: None,
loot: LootSpec::Nothing,
rtsim_entity: None,
projectile: None,
}
}
pub fn with_health(mut self, health: impl Into<Option<comp::Health>>) -> Self {
self.health = health.into();
self
}
pub fn with_poise(mut self, poise: comp::Poise) -> Self {
self.poise = poise;
self
}
pub fn with_agent(mut self, agent: impl Into<Option<comp::Agent>>) -> Self {
self.agent = agent.into();
self
}
pub fn with_anchor(mut self, anchor: comp::Anchor) -> Self {
self.anchor = Some(anchor);
self
}
pub fn with_rtsim(mut self, rtsim: RtSimEntity) -> Self {
self.rtsim_entity = Some(rtsim);
self
}
pub fn with_projectile(mut self, projectile: impl Into<Option<comp::Projectile>>) -> Self {
self.projectile = projectile.into();
self
}
pub fn with_scale(mut self, scale: comp::Scale) -> Self {
self.scale = scale;
self
}
pub fn with_inventory(mut self, inventory: comp::Inventory) -> Self {
self.inventory = inventory;
self
}
pub fn with_skill_set(mut self, skill_set: comp::SkillSet) -> Self {
self.skill_set = skill_set;
self
}
pub fn with_loot(mut self, loot: LootSpec<String>) -> Self {
self.loot = loot;
self
}
}
#[allow(clippy::large_enum_variant)] // TODO: Pending review in #587 #[allow(clippy::large_enum_variant)] // TODO: Pending review in #587
#[derive(strum::EnumDiscriminants)] #[derive(strum::EnumDiscriminants)]
#[strum_discriminants(repr(usize))] #[strum_discriminants(repr(usize))]
@ -137,26 +214,14 @@ pub enum ServerEvent {
// TODO: to avoid breakage when adding new fields, perhaps have an `NpcBuilder` type? // TODO: to avoid breakage when adding new fields, perhaps have an `NpcBuilder` type?
CreateNpc { CreateNpc {
pos: Pos, pos: Pos,
stats: comp::Stats, npc: NpcBuilder,
skill_set: comp::SkillSet,
health: Option<comp::Health>,
poise: comp::Poise,
inventory: comp::inventory::Inventory,
body: comp::Body,
agent: Option<comp::Agent>,
alignment: comp::Alignment,
scale: comp::Scale,
anchor: Option<comp::Anchor>,
loot: LootSpec<String>,
rtsim_entity: Option<RtSimEntity>,
projectile: Option<comp::Projectile>,
}, },
CreateShip { CreateShip {
pos: Pos, pos: Pos,
ship: comp::ship::Body, ship: comp::ship::Body,
mountable: bool, rtsim_entity: Option<RtSimVehicle>,
agent: Option<comp::Agent>, driver: Option<NpcBuilder>,
rtsim_entity: Option<RtSimEntity>, passangers: Vec<NpcBuilder>,
}, },
CreateWaypoint(Vec3<f32>), CreateWaypoint(Vec3<f32>),
ClientDisconnect(EcsEntity, DisconnectReason), ClientDisconnect(EcsEntity, DisconnectReason),

View File

@ -375,14 +375,8 @@ impl EntityInfo {
} }
#[must_use] #[must_use]
pub fn with_agent_mark(mut self, agent_mark: agent::Mark) -> Self { pub fn with_agent_mark(mut self, agent_mark: impl Into<Option<agent::Mark>>) -> Self {
self.agent_mark = Some(agent_mark); self.agent_mark = agent_mark.into();
self
}
#[must_use]
pub fn with_maybe_agent_mark(mut self, agent_mark: Option<agent::Mark>) -> Self {
self.agent_mark = agent_mark;
self self
} }
@ -442,15 +436,8 @@ impl EntityInfo {
/// map contains price+amount /// map contains price+amount
#[must_use] #[must_use]
pub fn with_economy(mut self, e: &SiteInformation) -> Self { pub fn with_economy<'a>(mut self, e: impl Into<Option<&'a SiteInformation>>) -> Self {
self.trading_information = Some(e.clone()); self.trading_information = e.into().cloned();
self
}
/// map contains price+amount
#[must_use]
pub fn with_maybe_economy(mut self, e: Option<&SiteInformation>) -> Self {
self.trading_information = e.cloned();
self self
} }

View File

@ -11,6 +11,8 @@ use crate::comp::dialogue::MoodState;
slotmap::new_key_type! { pub struct NpcId; } 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 SiteId; }
slotmap::new_key_type! { pub struct FactionId; } slotmap::new_key_type! { pub struct FactionId; }
@ -22,6 +24,13 @@ impl Component for RtSimEntity {
type Storage = specs::VecStorage<Self>; type Storage = specs::VecStorage<Self>;
} }
#[derive(Copy, Clone, Debug)]
pub struct RtSimVehicle(pub VehicleId);
impl Component for RtSimVehicle {
type Storage = specs::VecStorage<Self>;
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum RtSimEvent { pub enum RtSimEvent {
AddMemory(Memory), AddMemory(Memory),
@ -139,6 +148,8 @@ pub enum Profession {
Cultist, Cultist,
#[serde(rename = "10")] #[serde(rename = "10")]
Herbalist, Herbalist,
#[serde(rename = "11")]
Captain,
} }
impl Profession { impl Profession {
@ -155,6 +166,7 @@ impl Profession {
Self::Pirate => "Pirate".to_string(), Self::Pirate => "Pirate".to_string(),
Self::Cultist => "Cultist".to_string(), Self::Cultist => "Cultist".to_string(),
Self::Herbalist => "Herbalist".to_string(), Self::Herbalist => "Herbalist".to_string(),
Self::Captain => "Captain".to_string(),
} }
} }
} }

View File

@ -6,7 +6,7 @@ use crate::{
skillset::skills, skillset::skills,
Behavior, BehaviorCapability, CharacterState, Projectile, StateUpdate, Behavior, BehaviorCapability, CharacterState, Projectile, StateUpdate,
}, },
event::{LocalEvent, ServerEvent}, event::{LocalEvent, ServerEvent, NpcBuilder},
outcome::Outcome, outcome::Outcome,
skillset_builder::{self, SkillSetBuilder}, skillset_builder::{self, SkillSetBuilder},
states::{ states::{
@ -174,27 +174,22 @@ impl CharacterBehavior for Data {
// Send server event to create npc // Send server event to create npc
output_events.emit_server(ServerEvent::CreateNpc { output_events.emit_server(ServerEvent::CreateNpc {
pos: comp::Pos(collision_vector - Vec3::unit_z() * obstacle_z), pos: comp::Pos(collision_vector - Vec3::unit_z() * obstacle_z),
stats, npc: NpcBuilder::new(stats, body, comp::Alignment::Owned(*data.uid))
skill_set, .with_skill_set(skill_set)
health, .with_health(health)
poise: comp::Poise::new(body), .with_inventory(comp::Inventory::with_loadout(loadout, body))
inventory: comp::Inventory::with_loadout(loadout, body), .with_agent(
body, comp::Agent::from_body(&body)
agent: Some( .with_behavior(Behavior::from(BehaviorCapability::SPEAK))
comp::Agent::from_body(&body) .with_no_flee_if(true)
.with_behavior(Behavior::from(BehaviorCapability::SPEAK)) )
.with_no_flee_if(true), .with_scale(
), self
alignment: comp::Alignment::Owned(*data.uid), .static_data
scale: self .summon_info
.static_data .scale
.summon_info .unwrap_or(comp::Scale(1.0))
.scale ).with_projectile(projectile)
.unwrap_or(comp::Scale(1.0)),
anchor: None,
loot: crate::lottery::LootSpec::Nothing,
rtsim_entity: None,
projectile,
}); });
// Send local event used for frontend shenanigans // Send local event used for frontend shenanigans

View File

@ -23,12 +23,21 @@ use std::{
marker::PhantomData, marker::PhantomData,
}; };
#[derive(Copy, Clone, Serialize, Deserialize)] #[derive(Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum Actor { pub enum Actor {
Npc(NpcId), Npc(NpcId),
Character(common::character::CharacterId), Character(common::character::CharacterId),
} }
impl Actor {
pub fn npc(&self) -> Option<NpcId> {
match self {
Actor::Npc(id) => Some(*id),
Actor::Character(_) => None,
}
}
}
#[derive(Clone, Serialize, Deserialize)] #[derive(Clone, Serialize, Deserialize)]
pub struct Data { pub struct Data {
pub nature: Nature, pub nature: Nature,

View File

@ -2,9 +2,10 @@ use crate::ai::{Action, NpcCtx};
pub use common::rtsim::{NpcId, Profession}; pub use common::rtsim::{NpcId, Profession};
use common::{ use common::{
comp, comp,
rtsim::{FactionId, RtSimController, SiteId}, grid::Grid,
rtsim::{FactionId, RtSimController, SiteId, VehicleId},
store::Id, store::Id,
uid::Uid, uid::Uid, vol::RectVolSize,
}; };
use hashbrown::HashMap; use hashbrown::HashMap;
use rand::prelude::*; use rand::prelude::*;
@ -20,10 +21,12 @@ use std::{
}, },
}; };
use vek::*; use vek::*;
use world::{civ::Track, site::Site as WorldSite, util::RandomPerm}; use world::{civ::Track, site::Site as WorldSite, util::{RandomPerm, LOCALITY}};
use super::Actor;
#[derive(Copy, Clone, Debug, Default)] #[derive(Copy, Clone, Debug, Default)]
pub enum NpcMode { pub enum SimulationMode {
/// The NPC is unloaded and is being simulated via rtsim. /// The NPC is unloaded and is being simulated via rtsim.
#[default] #[default]
Simulated, Simulated,
@ -44,12 +47,24 @@ pub struct PathingMemory {
pub intersite_path: Option<(PathData<(Id<Track>, bool), SiteId>, usize)>, pub intersite_path: Option<(PathData<(Id<Track>, bool), SiteId>, usize)>,
} }
#[derive(Clone, Copy)]
pub enum NpcAction {
/// (wpos, speed_factor)
Goto(Vec3<f32>, f32),
}
pub struct Controller { pub struct Controller {
pub goto: Option<(Vec3<f32>, f32)>, pub action: Option<NpcAction>,
} }
impl Controller { impl Controller {
pub fn idle() -> Self { Self { goto: None } } pub fn idle() -> Self { Self { action: None } }
pub fn goto(wpos: Vec3<f32>, speed_factor: f32) -> Self {
Self {
action: Some(NpcAction::Goto(wpos, speed_factor)),
}
}
} }
pub struct Brain { pub struct Brain {
@ -67,20 +82,23 @@ pub struct Npc {
pub home: Option<SiteId>, pub home: Option<SiteId>,
pub faction: Option<FactionId>, pub faction: Option<FactionId>,
pub riding: Option<Riding>,
// Unpersisted state // Unpersisted state
#[serde(skip_serializing, skip_deserializing)] #[serde(skip_serializing, skip_deserializing)]
pub chunk_pos: Option<Vec2<i32>>,
#[serde(skip_serializing, skip_deserializing)]
pub current_site: Option<SiteId>, pub current_site: Option<SiteId>,
/// (wpos, speed_factor)
#[serde(skip_serializing, skip_deserializing)] #[serde(skip_serializing, skip_deserializing)]
pub goto: Option<(Vec3<f32>, f32)>, pub action: Option<NpcAction>,
/// Whether the NPC is in simulated or loaded mode (when rtsim is run on the /// Whether the NPC is in simulated or loaded mode (when rtsim is run on the
/// server, loaded corresponds to being within a loaded chunk). When in /// server, loaded corresponds to being within a loaded chunk). When in
/// loaded mode, the interactions of the NPC should not be simulated but /// loaded mode, the interactions of the NPC should not be simulated but
/// should instead be derived from the game. /// should instead be derived from the game.
#[serde(skip_serializing, skip_deserializing)] #[serde(skip_serializing, skip_deserializing)]
pub mode: NpcMode, pub mode: SimulationMode,
#[serde(skip_serializing, skip_deserializing)] #[serde(skip_serializing, skip_deserializing)]
pub brain: Option<Brain>, pub brain: Option<Brain>,
@ -94,9 +112,11 @@ impl Clone for Npc {
profession: self.profession.clone(), profession: self.profession.clone(),
home: self.home, home: self.home,
faction: self.faction, faction: self.faction,
riding: self.riding.clone(),
// Not persisted // Not persisted
chunk_pos: None,
current_site: Default::default(), current_site: Default::default(),
goto: Default::default(), action: Default::default(),
mode: Default::default(), mode: Default::default(),
brain: Default::default(), brain: Default::default(),
} }
@ -114,9 +134,11 @@ impl Npc {
profession: None, profession: None,
home: None, home: None,
faction: None, faction: None,
riding: None,
chunk_pos: None,
current_site: None, current_site: None,
goto: None, action: None,
mode: NpcMode::Simulated, mode: SimulationMode::Simulated,
brain: None, brain: None,
} }
} }
@ -131,6 +153,26 @@ impl Npc {
self self
} }
pub fn steering(mut self, vehicle: impl Into<Option<VehicleId>>) -> Self {
self.riding = vehicle.into().map(|vehicle| {
Riding {
vehicle,
steering: true,
}
});
self
}
pub fn riding(mut self, vehicle: impl Into<Option<VehicleId>>) -> Self {
self.riding = vehicle.into().map(|vehicle| {
Riding {
vehicle,
steering: false,
}
});
self
}
pub fn with_faction(mut self, faction: impl Into<Option<FactionId>>) -> Self { pub fn with_faction(mut self, faction: impl Into<Option<FactionId>>) -> Self {
self.faction = faction.into(); self.faction = faction.into();
self self
@ -147,12 +189,115 @@ impl Npc {
} }
#[derive(Clone, Serialize, Deserialize)] #[derive(Clone, Serialize, Deserialize)]
pub struct Npcs { pub struct Riding {
pub npcs: HopSlotMap<NpcId, Npc>, pub vehicle: VehicleId,
pub steering: bool,
} }
#[derive(Clone, Serialize, Deserialize)]
pub enum VehicleKind {
Airship,
Boat,
}
#[derive(Clone, Serialize, Deserialize)]
pub struct Vehicle {
pub wpos: Vec3<f32>,
pub kind: VehicleKind,
#[serde(skip_serializing, skip_deserializing)]
pub chunk_pos: Option<Vec2<i32>>,
#[serde(skip_serializing, skip_deserializing)]
pub driver: Option<Actor>,
#[serde(skip_serializing, skip_deserializing)]
// TODO: Find a way to detect riders when the vehicle is loaded
pub riders: Vec<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_serializing, skip_deserializing)]
pub mode: SimulationMode,
}
impl Vehicle {
pub fn new(wpos: Vec3<f32>, kind: VehicleKind) -> Self {
Self {
wpos,
kind,
chunk_pos: None,
driver: None,
riders: Vec::new(),
mode: SimulationMode::Simulated,
}
}
pub fn get_ship(&self) -> comp::ship::Body {
match self.kind {
VehicleKind::Airship => comp::ship::Body::DefaultAirship,
VehicleKind::Boat => comp::ship::Body::Galleon,
}
}
pub fn get_body(&self) -> comp::Body {
comp::Body::Ship(self.get_ship())
}
/// Max speed in block/s
pub fn get_speed(&self) -> f32 {
match self.kind {
VehicleKind::Airship => 15.0,
VehicleKind::Boat => 13.0,
}
}
}
#[derive(Default, Clone, Serialize, Deserialize)]
pub struct GridCell {
pub npcs: Vec<NpcId>,
pub vehicles: Vec<VehicleId>,
}
#[derive(Clone, Serialize, Deserialize)]
pub struct Npcs {
pub npcs: HopSlotMap<NpcId, Npc>,
pub vehicles: HopSlotMap<VehicleId, Vehicle>,
#[serde(skip, default = "construct_npc_grid")]
pub npc_grid: Grid<GridCell>,
}
fn construct_npc_grid() -> Grid<GridCell> { Grid::new(Vec2::zero(), Default::default()) }
impl Npcs { impl Npcs {
pub fn create(&mut self, npc: Npc) -> NpcId { self.npcs.insert(npc) } pub fn create_npc(&mut self, npc: Npc) -> NpcId { self.npcs.insert(npc) }
pub fn create_vehicle(&mut self, vehicle: Vehicle) -> VehicleId { self.vehicles.insert(vehicle) }
/// Queries nearby npcs, not garantueed to work if radius > 32.0
pub fn nearby(&self, wpos: Vec2<f32>, radius: f32) -> impl Iterator<Item = NpcId> + '_ {
let chunk_pos = wpos.as_::<i32>() / common::terrain::TerrainChunkSize::RECT_SIZE.as_::<i32>();
let r_sqr = radius * radius;
LOCALITY
.into_iter()
.filter_map(move |neighbor| {
self
.npc_grid
.get(chunk_pos + neighbor)
.map(|cell| {
cell.npcs.iter()
.copied()
.filter(|npc| {
self.npcs.get(*npc)
.map_or(false, |npc| npc.wpos.xy().distance_squared(wpos) < r_sqr)
})
.collect::<Vec<_>>()
})
})
.flatten()
}
} }
impl Deref for Npcs { impl Deref for Npcs {

View File

@ -3,12 +3,13 @@ pub mod site;
use crate::data::{ use crate::data::{
faction::{Faction, Factions}, faction::{Faction, Factions},
npc::{Npc, Npcs, Profession}, npc::{Npc, Npcs, Profession, Vehicle, VehicleKind},
site::{Site, Sites}, site::{Site, Sites},
Data, Nature, Data, Nature,
}; };
use common::{ use common::{
resources::TimeOfDay, rtsim::WorldSettings, terrain::TerrainChunkSize, vol::RectVolSize, grid::Grid, resources::TimeOfDay, rtsim::WorldSettings, terrain::TerrainChunkSize,
vol::RectVolSize,
}; };
use hashbrown::HashMap; use hashbrown::HashMap;
use rand::prelude::*; use rand::prelude::*;
@ -28,6 +29,8 @@ impl Data {
nature: Nature::generate(world), nature: Nature::generate(world),
npcs: Npcs { npcs: Npcs {
npcs: Default::default(), npcs: Default::default(),
vehicles: Default::default(),
npc_grid: Grid::new(Vec2::zero(), Default::default()),
}, },
sites: Sites { sites: Sites {
sites: Default::default(), sites: Default::default(),
@ -75,7 +78,6 @@ impl Data {
// TODO: Stupid // TODO: Stupid
.filter(|(_, site)| site.world_site.map_or(false, |ws| .filter(|(_, site)| site.world_site.map_or(false, |ws|
matches!(&index.sites.get(ws).kind, SiteKind::Refactor(_)))) .skip(1) matches!(&index.sites.get(ws).kind, SiteKind::Refactor(_)))) .skip(1)
.take(1)
{ {
let Some(good_or_evil) = site let Some(good_or_evil) = site
.faction .faction
@ -91,7 +93,7 @@ impl Data {
}; };
if good_or_evil { if good_or_evil {
for _ in 0..32 { for _ in 0..32 {
this.npcs.create( this.npcs.create_npc(
Npc::new(rng.gen(), rand_wpos(&mut rng)) Npc::new(rng.gen(), rand_wpos(&mut rng))
.with_faction(site.faction) .with_faction(site.faction)
.with_home(site_id) .with_home(site_id)
@ -109,7 +111,7 @@ impl Data {
} }
} else { } else {
for _ in 0..15 { for _ in 0..15 {
this.npcs.create( this.npcs.create_npc(
Npc::new(rng.gen(), rand_wpos(&mut rng)) Npc::new(rng.gen(), rand_wpos(&mut rng))
.with_faction(site.faction) .with_faction(site.faction)
.with_home(site_id) .with_home(site_id)
@ -119,12 +121,18 @@ impl Data {
); );
} }
} }
this.npcs.create( this.npcs.create_npc(
Npc::new(rng.gen(), rand_wpos(&mut rng)) Npc::new(rng.gen(), rand_wpos(&mut rng))
.with_home(site_id) .with_home(site_id)
.with_profession(Profession::Merchant), .with_profession(Profession::Merchant),
); );
let wpos = rand_wpos(&mut rng) + Vec3::unit_z() * 50.0;
let vehicle_id = this.npcs.create_vehicle(Vehicle::new(wpos, VehicleKind::Airship));
this.npcs.create_npc(Npc::new(rng.gen(), wpos).with_home(site_id).with_profession(Profession::Captain).steering(vehicle_id));
} }
info!("Generated {} rtsim NPCs.", this.npcs.len()); info!("Generated {} rtsim NPCs.", this.npcs.len());
this this

View File

@ -3,7 +3,7 @@ use std::{collections::VecDeque, hash::BuildHasherDefault};
use crate::{ use crate::{
ai::{casual, choose, finish, important, just, now, seq, until, urgent, watch, Action, NpcCtx}, ai::{casual, choose, finish, important, just, now, seq, until, urgent, watch, Action, NpcCtx},
data::{ data::{
npc::{Brain, Controller, Npc, NpcId, PathData, PathingMemory}, npc::{Brain, Controller, Npc, NpcId, PathData, PathingMemory, VehicleKind},
Sites, Sites,
}, },
event::OnTick, event::OnTick,
@ -14,7 +14,7 @@ use common::{
path::Path, path::Path,
rtsim::{Profession, SiteId}, rtsim::{Profession, SiteId},
store::Id, store::Id,
terrain::TerrainChunkSize, terrain::{TerrainChunkSize, SiteKindMeta},
time::DayPeriod, time::DayPeriod,
vol::RectVolSize, vol::RectVolSize,
}; };
@ -32,7 +32,7 @@ use world::{
civ::{self, Track}, civ::{self, Track},
site::{Site as WorldSite, SiteKind}, site::{Site as WorldSite, SiteKind},
site2::{self, PlotKind, TileKind}, site2::{self, PlotKind, TileKind},
IndexRef, World, IndexRef, World, util::NEIGHBORS,
}; };
pub struct NpcAi; pub struct NpcAi;
@ -221,7 +221,7 @@ impl Rule for NpcAi {
data.npcs data.npcs
.iter_mut() .iter_mut()
.map(|(npc_id, npc)| { .map(|(npc_id, npc)| {
let controller = Controller { goto: npc.goto }; let controller = Controller { action: npc.action };
let brain = npc.brain.take().unwrap_or_else(|| Brain { let brain = npc.brain.take().unwrap_or_else(|| Brain {
action: Box::new(think().repeat()), action: Box::new(think().repeat()),
}); });
@ -253,7 +253,7 @@ impl Rule for NpcAi {
let mut data = ctx.state.data_mut(); let mut data = ctx.state.data_mut();
for (npc_id, controller, brain) in npc_data { for (npc_id, controller, brain) in npc_data {
data.npcs[npc_id].goto = controller.goto; data.npcs[npc_id].action = controller.action;
data.npcs[npc_id].brain = Some(brain); data.npcs[npc_id].brain = Some(brain);
} }
@ -331,7 +331,7 @@ fn goto(wpos: Vec3<f32>, speed_factor: f32, goal_dist: f32) -> impl Action {
let waypoint = let waypoint =
waypoint.get_or_insert_with(|| ctx.npc.wpos + (rpos / len) * len.min(STEP_DIST)); waypoint.get_or_insert_with(|| ctx.npc.wpos + (rpos / len) * len.min(STEP_DIST));
ctx.controller.goto = Some((*waypoint, speed_factor)); *ctx.controller = Controller::goto(*waypoint, speed_factor);
}) })
.repeat() .repeat()
.stop_if(move |ctx| ctx.npc.wpos.xy().distance_squared(wpos.xy()) < goal_dist.powi(2)) .stop_if(move |ctx| ctx.npc.wpos.xy().distance_squared(wpos.xy()) < goal_dist.powi(2))
@ -616,9 +616,126 @@ fn villager(visiting_site: SiteId) -> impl Action {
.debug(move || format!("villager at site {:?}", visiting_site)) .debug(move || format!("villager at site {:?}", visiting_site))
} }
fn follow(npc: NpcId, distance: f32) -> impl Action {
const STEP_DIST: f32 = 1.0;
now(move |ctx| {
if let Some(npc) = ctx.state.data().npcs.get(npc) {
let d = npc.wpos.xy() - ctx.npc.wpos.xy();
let len = d.magnitude();
let dir = d / len;
let wpos = ctx.npc.wpos.xy() + dir * STEP_DIST.min(len - distance);
goto_2d(wpos, 1.0, distance).boxed()
} else {
// The npc we're trying to follow doesn't exist.
finish().boxed()
}
})
.repeat()
.debug(move || format!("Following npc({npc:?})"))
.map(|_| {})
}
fn chunk_path(from: Vec2<i32>, to: Vec2<i32>, chunk_height: impl Fn(Vec2<i32>) -> Option<i32>) -> Box<dyn Action> {
let heuristics = |(p, _): &(Vec2<i32>, i32)| p.distance_squared(to) as f32;
let start = (from, chunk_height(from).unwrap());
let mut astar = Astar::new(
1000,
start,
heuristics,
BuildHasherDefault::<FxHasher64>::default(),
);
let path = astar.poll(
1000,
heuristics,
|&(p, _)| NEIGHBORS.into_iter().map(move |n| p + n).filter_map(|p| Some((p, chunk_height(p)?))),
|(p0, h0), (p1, h1)| {
let diff = ((p0 - p1).as_() * TerrainChunkSize::RECT_SIZE.as_()).with_z((h0 - h1) as f32);
diff.magnitude_squared()
},
|(e, _)| *e == to
);
let path = match path {
PathResult::Exhausted(p) | PathResult::Path(p) => p,
_ => return finish().boxed(),
};
let len = path.len();
seq(
path
.into_iter()
.enumerate()
.map(move |(i, (chunk_pos, height))| {
let wpos = TerrainChunkSize::center_wpos(chunk_pos).with_z(height).as_();
goto(wpos, 1.0, 5.0).debug(move || format!("chunk path {i}/{len} chunk: {chunk_pos}, height: {height}"))
})
).boxed()
}
fn pilot() -> impl Action {
// Travel between different towns in a straight line
now(|ctx| {
let data = &*ctx.state.data();
let site = data.sites.iter()
.filter(|(id, _)| Some(*id) != ctx.npc.current_site)
.filter(|(_, site)| {
site.world_site
.and_then(|site| ctx.index.sites.get(site).kind.convert_to_meta())
.map_or(false, |meta| matches!(meta, SiteKindMeta::Settlement(_)))
})
.choose(&mut thread_rng());
if let Some((_id, site)) = site {
let start_chunk = ctx.npc.wpos.xy().as_::<i32>() / TerrainChunkSize::RECT_SIZE.as_::<i32>();
let end_chunk = site.wpos / TerrainChunkSize::RECT_SIZE.as_::<i32>();
chunk_path(start_chunk, end_chunk, |chunk| {
ctx.world.sim().get_alt_approx(TerrainChunkSize::center_wpos(chunk)).map(|f| {
(f + 150.0) as i32
})
})
} else {
finish().boxed()
}
})
.repeat()
.map(|_| ())
}
fn captain() -> impl Action {
// For now just randomly travel the sea
now(|ctx| {
let chunk = ctx.npc.wpos.xy().as_::<i32>() / TerrainChunkSize::RECT_SIZE.as_::<i32>();
if let Some(chunk) = NEIGHBORS
.into_iter()
.map(|neighbor| chunk + neighbor)
.filter(|neighbor| ctx.world.sim().get(*neighbor).map_or(false, |c| c.river.river_kind.is_some()))
.choose(&mut thread_rng())
{
let wpos = TerrainChunkSize::center_wpos(chunk);
let wpos = wpos.as_().with_z(ctx.world.sim().get_interpolated(wpos, |chunk| chunk.water_alt).unwrap_or(0.0));
goto(wpos, 0.7, 5.0).boxed()
} else {
idle().boxed()
}
})
.repeat().map(|_| ())
}
fn think() -> impl Action { fn think() -> impl Action {
choose(|ctx| { choose(|ctx| {
if matches!( if let Some(riding) = &ctx.npc.riding {
if riding.steering {
if let Some(vehicle) = ctx.state.data().npcs.vehicles.get(riding.vehicle) {
match vehicle.kind {
VehicleKind::Airship => important(pilot()),
VehicleKind::Boat => important(captain()),
}
} else {
casual(finish())
}
} else {
important(idle())
}
} else if matches!(
ctx.npc.profession, ctx.npc.profession,
Some(Profession::Adventurer(_) | Profession::Merchant) Some(Profession::Adventurer(_) | Profession::Merchant)
) { ) {

View File

@ -1,5 +1,9 @@
use crate::{data::npc::NpcMode, event::OnTick, RtState, Rule, RuleError}; use crate::{
use common::{terrain::TerrainChunkSize, vol::RectVolSize}; data::npc::SimulationMode,
event::{OnSetup, OnTick},
RtState, Rule, RuleError,
};
use common::{terrain::TerrainChunkSize, vol::RectVolSize, grid::Grid};
use tracing::info; use tracing::info;
use vek::*; use vek::*;
@ -7,9 +11,46 @@ pub struct SimulateNpcs;
impl Rule for SimulateNpcs { impl Rule for SimulateNpcs {
fn start(rtstate: &mut RtState) -> Result<Self, RuleError> { fn start(rtstate: &mut RtState) -> Result<Self, RuleError> {
rtstate.bind::<Self, OnSetup>(|ctx| {
let data = &mut *ctx.state.data_mut();
data.npcs.npc_grid = Grid::new(ctx.world.sim().get_size().as_(), Default::default());
for (npc_id, npc) in data.npcs.npcs.iter() {
if let Some(ride) = &npc.riding {
if let Some(vehicle) = data.npcs.vehicles.get_mut(ride.vehicle) {
let actor = crate::data::Actor::Npc(npc_id);
vehicle.riders.push(actor);
if ride.steering {
if vehicle.driver.replace(actor).is_some() {
panic!("Replaced driver");
}
}
}
}
}
});
rtstate.bind::<Self, OnTick>(|ctx| { rtstate.bind::<Self, OnTick>(|ctx| {
let data = &mut *ctx.state.data_mut(); let data = &mut *ctx.state.data_mut();
for npc in data.npcs.values_mut() { for (vehicle_id, vehicle) in data.npcs.vehicles.iter_mut() {
let chunk_pos =
vehicle.wpos.xy().as_::<i32>() / TerrainChunkSize::RECT_SIZE.as_::<i32>();
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 // Update the NPC's current site, if any
npc.current_site = ctx npc.current_site = ctx
.world .world
@ -17,30 +58,102 @@ impl Rule for SimulateNpcs {
.get(npc.wpos.xy().as_::<i32>() / TerrainChunkSize::RECT_SIZE.as_()) .get(npc.wpos.xy().as_::<i32>() / TerrainChunkSize::RECT_SIZE.as_())
.and_then(|chunk| data.sites.world_site_map.get(chunk.sites.first()?).copied()); .and_then(|chunk| data.sites.world_site_map.get(chunk.sites.first()?).copied());
// Simulate the NPC's movement and interactions let chunk_pos =
if matches!(npc.mode, NpcMode::Simulated) { npc.wpos.xy().as_::<i32>() / TerrainChunkSize::RECT_SIZE.as_::<i32>();
let body = npc.get_body(); if npc.chunk_pos != Some(chunk_pos) {
if let Some(cell) = npc
// Move NPCs if they have a target destination .chunk_pos
if let Some((target, speed_factor)) = npc.goto { .and_then(|chunk_pos| data.npcs.npc_grid.get_mut(chunk_pos))
let diff = target.xy() - npc.wpos.xy(); {
let dist2 = diff.magnitude_squared(); if let Some(index) = cell.npcs.iter().position(|id| *id == npc_id) {
cell.npcs.swap_remove(index);
if dist2 > 0.5f32.powi(2) {
npc.wpos += (diff
* (body.max_speed_approx() * speed_factor * ctx.event.dt
/ dist2.sqrt())
.min(1.0))
.with_z(0.0);
} }
} }
npc.chunk_pos = Some(chunk_pos);
if let Some(cell) = data.npcs.npc_grid.get_mut(chunk_pos) {
cell.npcs.push(npc_id);
}
}
// Simulate the NPC's movement and interactions
if matches!(npc.mode, SimulationMode::Simulated) {
let body = npc.get_body();
if let Some(riding) = &npc.riding {
if let Some(vehicle) = data.npcs.vehicles.get_mut(riding.vehicle) {
if let Some(action) = npc.action && riding.steering {
match action {
crate::data::npc::NpcAction::Goto(target, speed_factor) => {
let diff = target.xy() - vehicle.wpos.xy();
let dist2 = diff.magnitude_squared();
if dist2 > 0.5f32.powi(2) {
let mut wpos = vehicle.wpos + (diff
* (vehicle.get_speed() * speed_factor * ctx.event.dt
/ dist2.sqrt())
.min(1.0))
.with_z(0.0);
let is_valid = match vehicle.kind {
crate::data::npc::VehicleKind::Airship => true,
crate::data::npc::VehicleKind::Boat => {
let chunk_pos = wpos.xy().as_::<i32>() / TerrainChunkSize::RECT_SIZE.as_::<i32>();
ctx.world.sim().get(chunk_pos).map_or(true, |f| f.river.river_kind.is_some())
},
};
if is_valid {
match vehicle.kind {
crate::data::npc::VehicleKind::Airship => {
if let Some(alt) = ctx.world.sim().get_alt_approx(wpos.xy().as_()).filter(|alt| wpos.z < *alt) {
wpos.z = alt;
}
},
crate::data::npc::VehicleKind::Boat => {
wpos.z = ctx
.world
.sim()
.get_interpolated(wpos.xy().map(|e| e as i32), |chunk| chunk.water_alt)
.unwrap_or(0.0);
},
}
vehicle.wpos = wpos;
}
}
}
}
}
npc.wpos = vehicle.wpos;
} else {
// Vehicle doens't exist anymore
npc.riding = None;
}
}
// Move NPCs if they have a target destination
else if let Some(action) = npc.action {
match action {
crate::data::npc::NpcAction::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
* (body.max_speed_approx() * speed_factor * ctx.event.dt
/ dist2.sqrt())
.min(1.0))
.with_z(0.0);
}
},
}
// Make sure NPCs remain on the surface
npc.wpos.z = ctx
.world
.sim()
.get_alt_approx(npc.wpos.xy().map(|e| e as i32))
.unwrap_or(0.0);
}
// Make sure NPCs remain on the surface
npc.wpos.z = ctx
.world
.sim()
.get_alt_approx(npc.wpos.xy().map(|e| e as i32))
.unwrap_or(0.0);
} }
} }
}); });

View File

@ -10,7 +10,7 @@ use common::{
Vel, Vel,
}, },
link::Is, link::Is,
mounting::Mount, mounting::{Mount, Rider},
path::TraversalConfig, path::TraversalConfig,
resources::{DeltaTime, Time, TimeOfDay}, resources::{DeltaTime, Time, TimeOfDay},
rtsim::RtSimEntity, rtsim::RtSimEntity,
@ -233,6 +233,7 @@ pub struct ReadData<'a> {
pub alignments: ReadStorage<'a, Alignment>, pub alignments: ReadStorage<'a, Alignment>,
pub bodies: ReadStorage<'a, Body>, pub bodies: ReadStorage<'a, Body>,
pub is_mounts: ReadStorage<'a, Is<Mount>>, pub is_mounts: ReadStorage<'a, Is<Mount>>,
pub is_riders: ReadStorage<'a, Is<Rider>>,
pub time_of_day: Read<'a, TimeOfDay>, pub time_of_day: Read<'a, TimeOfDay>,
pub light_emitter: ReadStorage<'a, LightEmitter>, pub light_emitter: ReadStorage<'a, LightEmitter>,
#[cfg(feature = "worldgen")] #[cfg(feature = "worldgen")]

View File

@ -1480,7 +1480,7 @@ fn handle_spawn_airship(
let ship = comp::ship::Body::random_airship_with(&mut rng); let ship = comp::ship::Body::random_airship_with(&mut rng);
let mut builder = server let mut builder = server
.state .state
.create_ship(pos, ship, |ship| ship.make_collider(), true) .create_ship(pos, ship, |ship| ship.make_collider())
.with(LightEmitter { .with(LightEmitter {
col: Rgb::new(1.0, 0.65, 0.2), col: Rgb::new(1.0, 0.65, 0.2),
strength: 2.0, strength: 2.0,
@ -1488,7 +1488,7 @@ fn handle_spawn_airship(
animated: true, animated: true,
}); });
if let Some(pos) = destination { if let Some(pos) = destination {
let (kp, ki, kd) = comp::agent::pid_coefficients(&comp::Body::Ship(ship)); let (kp, ki, kd) = comp::agent::pid_coefficients(&comp::Body::Ship(ship)).unwrap_or((1.0, 0.0, 0.0));
fn pure_z(sp: Vec3<f32>, pv: Vec3<f32>) -> f32 { (sp - pv).z } fn pure_z(sp: Vec3<f32>, pv: Vec3<f32>) -> f32 { (sp - pv).z }
let agent = comp::Agent::from_body(&comp::Body::Ship(ship)) let agent = comp::Agent::from_body(&comp::Body::Ship(ship))
.with_destination(pos) .with_destination(pos)
@ -1528,7 +1528,7 @@ fn handle_spawn_ship(
let ship = comp::ship::Body::random_ship_with(&mut rng); let ship = comp::ship::Body::random_ship_with(&mut rng);
let mut builder = server let mut builder = server
.state .state
.create_ship(pos, ship, |ship| ship.make_collider(), true) .create_ship(pos, ship, |ship| ship.make_collider())
.with(LightEmitter { .with(LightEmitter {
col: Rgb::new(1.0, 0.65, 0.2), col: Rgb::new(1.0, 0.65, 0.2),
strength: 2.0, strength: 2.0,
@ -1536,7 +1536,7 @@ fn handle_spawn_ship(
animated: true, animated: true,
}); });
if let Some(pos) = destination { if let Some(pos) = destination {
let (kp, ki, kd) = comp::agent::pid_coefficients(&comp::Body::Ship(ship)); let (kp, ki, kd) = comp::agent::pid_coefficients(&comp::Body::Ship(ship)).unwrap_or((1.0, 0.0, 0.0));
fn pure_z(sp: Vec3<f32>, pv: Vec3<f32>) -> f32 { (sp - pv).z } fn pure_z(sp: Vec3<f32>, pv: Vec3<f32>) -> f32 { (sp - pv).z }
let agent = comp::Agent::from_body(&comp::Body::Ship(ship)) let agent = comp::Agent::from_body(&comp::Body::Ship(ship))
.with_destination(pos) .with_destination(pos)
@ -1581,7 +1581,6 @@ fn handle_make_volume(
comp::Pos(pos.0 + Vec3::unit_z() * 50.0), comp::Pos(pos.0 + Vec3::unit_z() * 50.0),
ship, ship,
move |_| collider, move |_| collider,
true,
) )
.build(); .build();

View File

@ -14,14 +14,14 @@ use common::{
LightEmitter, Object, Ori, PidController, Poise, Pos, Projectile, Scale, SkillSet, Stats, LightEmitter, Object, Ori, PidController, Poise, Pos, Projectile, Scale, SkillSet, Stats,
TradingBehavior, Vel, WaypointArea, TradingBehavior, Vel, WaypointArea,
}, },
event::{EventBus, UpdateCharacterMetadata}, event::{EventBus, UpdateCharacterMetadata, NpcBuilder},
lottery::LootSpec, lottery::LootSpec,
outcome::Outcome, outcome::Outcome,
resources::{Secs, Time}, resources::{Secs, Time},
rtsim::RtSimEntity, rtsim::{RtSimEntity, RtSimVehicle},
uid::Uid, uid::Uid,
util::Dir, util::Dir,
ViewDistances, ViewDistances, mounting::Mounting,
}; };
use common_net::{msg::ServerGeneral, sync::WorldSyncExt}; use common_net::{msg::ServerGeneral, sync::WorldSyncExt};
use specs::{Builder, Entity as EcsEntity, WorldExt}; use specs::{Builder, Entity as EcsEntity, WorldExt};
@ -94,60 +94,47 @@ pub fn handle_loaded_character_data(
pub fn handle_create_npc( pub fn handle_create_npc(
server: &mut Server, server: &mut Server,
pos: Pos, pos: Pos,
stats: Stats, mut npc: NpcBuilder,
skill_set: SkillSet, ) -> EcsEntity {
health: Option<Health>,
poise: Poise,
inventory: Inventory,
body: Body,
agent: impl Into<Option<Agent>>,
alignment: Alignment,
scale: Scale,
loot: LootSpec<String>,
home_chunk: Option<Anchor>,
rtsim_entity: Option<RtSimEntity>,
projectile: Option<Projectile>,
) {
let entity = server let entity = server
.state .state
.create_npc(pos, stats, skill_set, health, poise, inventory, body) .create_npc(pos, npc.stats, npc.skill_set, npc.health, npc.poise, npc.inventory, npc.body)
.with(scale); .with(npc.scale);
let mut agent = agent.into(); if let Some(agent) = &mut npc.agent {
if let Some(agent) = &mut agent { if let Alignment::Owned(_) = &npc.alignment {
if let Alignment::Owned(_) = &alignment {
agent.behavior.allow(BehaviorCapability::TRADE); agent.behavior.allow(BehaviorCapability::TRADE);
agent.behavior.trading_behavior = TradingBehavior::AcceptFood; agent.behavior.trading_behavior = TradingBehavior::AcceptFood;
} }
} }
let entity = entity.with(alignment); let entity = entity.with(npc.alignment);
let entity = if let Some(agent) = agent { let entity = if let Some(agent) = npc.agent {
entity.with(agent) entity.with(agent)
} else { } else {
entity entity
}; };
let entity = if let Some(drop_item) = loot.to_item() { let entity = if let Some(drop_item) = npc.loot.to_item() {
entity.with(ItemDrop(drop_item)) entity.with(ItemDrop(drop_item))
} else { } else {
entity entity
}; };
let entity = if let Some(home_chunk) = home_chunk { let entity = if let Some(home_chunk) = npc.anchor {
entity.with(home_chunk) entity.with(home_chunk)
} else { } else {
entity entity
}; };
let entity = if let Some(rtsim_entity) = rtsim_entity { let entity = if let Some(rtsim_entity) = npc.rtsim_entity {
entity.with(rtsim_entity) entity.with(rtsim_entity)
} else { } else {
entity entity
}; };
let entity = if let Some(projectile) = projectile { let entity = if let Some(projectile) = npc.projectile {
entity.with(projectile) entity.with(projectile)
} else { } else {
entity entity
@ -156,7 +143,7 @@ pub fn handle_create_npc(
let new_entity = entity.build(); let new_entity = entity.build();
// Add to group system if a pet // Add to group system if a pet
if let comp::Alignment::Owned(owner_uid) = alignment { if let comp::Alignment::Owned(owner_uid) = npc.alignment {
let state = server.state(); let state = server.state();
let clients = state.ecs().read_storage::<Client>(); let clients = state.ecs().read_storage::<Client>();
let uids = state.ecs().read_storage::<Uid>(); let uids = state.ecs().read_storage::<Uid>();
@ -187,7 +174,7 @@ pub fn handle_create_npc(
}, },
); );
} }
} else if let Some(group) = match alignment { } else if let Some(group) = match npc.alignment {
Alignment::Wild => None, Alignment::Wild => None,
Alignment::Passive => None, Alignment::Passive => None,
Alignment::Enemy => Some(comp::group::ENEMY), Alignment::Enemy => Some(comp::group::ENEMY),
@ -196,19 +183,23 @@ pub fn handle_create_npc(
} { } {
let _ = server.state.ecs().write_storage().insert(new_entity, group); let _ = server.state.ecs().write_storage().insert(new_entity, group);
} }
new_entity
} }
pub fn handle_create_ship( pub fn handle_create_ship(
server: &mut Server, server: &mut Server,
pos: Pos, pos: Pos,
ship: comp::ship::Body, ship: comp::ship::Body,
mountable: bool, rtsim_vehicle: Option<RtSimVehicle>,
agent: Option<Agent>, driver: Option<NpcBuilder>,
rtsim_entity: Option<RtSimEntity>, passangers: Vec<NpcBuilder>,
) { ) {
let mut entity = server let mut entity = server
.state .state
.create_ship(pos, ship, |ship| ship.make_collider(), mountable); .create_ship(pos, ship, |ship| ship.make_collider());
/*
if let Some(mut agent) = agent { if let Some(mut agent) = agent {
let (kp, ki, kd) = pid_coefficients(&Body::Ship(ship)); let (kp, ki, kd) = pid_coefficients(&Body::Ship(ship));
fn pure_z(sp: Vec3<f32>, pv: Vec3<f32>) -> f32 { (sp - pv).z } fn pure_z(sp: Vec3<f32>, pv: Vec3<f32>) -> f32 { (sp - pv).z }
@ -216,10 +207,33 @@ pub fn handle_create_ship(
agent.with_position_pid_controller(PidController::new(kp, ki, kd, pos.0, 0.0, pure_z)); agent.with_position_pid_controller(PidController::new(kp, ki, kd, pos.0, 0.0, pure_z));
entity = entity.with(agent); entity = entity.with(agent);
} }
if let Some(rtsim_entity) = rtsim_entity { */
entity = entity.with(rtsim_entity); if let Some(rtsim_vehicle) = rtsim_vehicle {
entity = entity.with(rtsim_vehicle);
}
let entity = entity.build();
if let Some(driver) = driver {
let npc_entity = handle_create_npc(server, pos, driver);
let uids = server.state.ecs().read_storage::<Uid>();
if let (Some(rider_uid), Some(mount_uid)) =
(uids.get(npc_entity).copied(), uids.get(entity).copied())
{
drop(uids);
server.state.link(Mounting {
mount: mount_uid,
rider: rider_uid,
}).expect("Failed to link driver to ship");
} else {
panic!("Couldn't get Uid from newly created ship and npc");
}
}
for passanger in passangers {
handle_create_npc(server, Pos(pos.0 + Vec3::unit_z() * 5.0), passanger);
} }
entity.build();
} }
pub fn handle_shoot( pub fn handle_shoot(

View File

@ -190,43 +190,21 @@ impl Server {
}, },
ServerEvent::CreateNpc { ServerEvent::CreateNpc {
pos, pos,
stats, npc,
skill_set, } => {
health, handle_create_npc(
poise, self,
inventory, pos,
body, npc,
agent, );
alignment, },
scale,
anchor: home_chunk,
loot,
rtsim_entity,
projectile,
} => handle_create_npc(
self,
pos,
stats,
skill_set,
health,
poise,
inventory,
body,
agent,
alignment,
scale,
loot,
home_chunk,
rtsim_entity,
projectile,
),
ServerEvent::CreateShip { ServerEvent::CreateShip {
pos, pos,
ship, ship,
mountable,
agent,
rtsim_entity, rtsim_entity,
} => handle_create_ship(self, pos, ship, mountable, agent, rtsim_entity), driver,
passangers,
} => handle_create_ship(self, pos, ship, rtsim_entity, driver, passangers),
ServerEvent::CreateWaypoint(pos) => handle_create_waypoint(self, pos), ServerEvent::CreateWaypoint(pos) => handle_create_waypoint(self, pos),
ServerEvent::ClientDisconnect(entity, reason) => { ServerEvent::ClientDisconnect(entity, reason) => {
frontend_events.push(handle_client_disconnect(self, entity, reason, false)) frontend_events.push(handle_client_disconnect(self, entity, reason, false))

View File

@ -80,8 +80,8 @@ use common::{
comp, comp,
event::{EventBus, ServerEvent}, event::{EventBus, ServerEvent},
resources::{BattleMode, GameMode, Time, TimeOfDay}, resources::{BattleMode, GameMode, Time, TimeOfDay},
rtsim::RtSimEntity,
shared_server_config::ServerConstants, shared_server_config::ServerConstants,
rtsim::{RtSimEntity, RtSimVehicle},
slowjob::SlowJobPool, slowjob::SlowJobPool,
terrain::{Block, TerrainChunk, TerrainChunkSize}, terrain::{Block, TerrainChunk, TerrainChunkSize},
vol::RectRasterableVol, vol::RectRasterableVol,
@ -387,6 +387,7 @@ impl Server {
state.ecs_mut().register::<login_provider::PendingLogin>(); state.ecs_mut().register::<login_provider::PendingLogin>();
state.ecs_mut().register::<RepositionOnChunkLoad>(); state.ecs_mut().register::<RepositionOnChunkLoad>();
state.ecs_mut().register::<RtSimEntity>(); state.ecs_mut().register::<RtSimEntity>();
state.ecs_mut().register::<RtSimVehicle>();
// Load banned words list // Load banned words list
let banned_words = settings.moderation.load_banned_words(data_dir); let banned_words = settings.moderation.load_banned_words(data_dir);
@ -873,6 +874,19 @@ impl Server {
.write_resource::<rtsim2::RtSim>() .write_resource::<rtsim2::RtSim>()
.hook_rtsim_entity_unload(rtsim_entity); .hook_rtsim_entity_unload(rtsim_entity);
} }
#[cfg(feature = "worldgen")]
if let Some(rtsim_vehicle) = self
.state
.ecs()
.read_storage::<RtSimVehicle>()
.get(entity)
.copied()
{
self.state
.ecs()
.write_resource::<rtsim2::RtSim>()
.hook_rtsim_vehicle_unload(rtsim_vehicle);
}
if let Err(e) = self.state.delete_entity_recorded(entity) { if let Err(e) = self.state.delete_entity_recorded(entity) {
error!(?e, "Failed to delete agent outside the terrain"); error!(?e, "Failed to delete agent outside the terrain");

View File

@ -4,7 +4,7 @@ pub mod tick;
use common::{ use common::{
grid::Grid, grid::Grid,
rtsim::{ChunkResource, RtSimEntity, WorldSettings}, rtsim::{ChunkResource, RtSimEntity, WorldSettings, RtSimVehicle},
slowjob::SlowJobPool, slowjob::SlowJobPool,
terrain::{Block, TerrainChunk}, terrain::{Block, TerrainChunk},
vol::RectRasterableVol, vol::RectRasterableVol,
@ -12,7 +12,7 @@ use common::{
use common_ecs::{dispatch, System}; use common_ecs::{dispatch, System};
use enum_map::EnumMap; use enum_map::EnumMap;
use rtsim2::{ use rtsim2::{
data::{npc::NpcMode, Data, ReadError}, data::{npc::SimulationMode, Data, ReadError},
event::{OnDeath, OnSetup}, event::{OnDeath, OnSetup},
rule::Rule, rule::Rule,
RtState, RtState,
@ -150,7 +150,13 @@ impl RtSim {
pub fn hook_rtsim_entity_unload(&mut self, entity: RtSimEntity) { pub fn hook_rtsim_entity_unload(&mut self, entity: RtSimEntity) {
if let Some(npc) = self.state.data_mut().npcs.get_mut(entity.0) { if let Some(npc) = self.state.data_mut().npcs.get_mut(entity.0) {
npc.mode = NpcMode::Simulated; npc.mode = SimulationMode::Simulated;
}
}
pub fn hook_rtsim_vehicle_unload(&mut self, entity: RtSimVehicle) {
if let Some(vehicle) = self.state.data_mut().npcs.vehicles.get_mut(entity.0) {
vehicle.mode = SimulationMode::Simulated;
} }
} }

View File

@ -3,19 +3,19 @@
use super::*; use super::*;
use crate::sys::terrain::NpcData; use crate::sys::terrain::NpcData;
use common::{ use common::{
comp::{self, inventory::loadout::Loadout, skillset::skills}, comp::{self, inventory::loadout::Loadout, skillset::skills, Body, Agent},
event::{EventBus, ServerEvent}, event::{EventBus, ServerEvent, NpcBuilder},
generation::{BodyBuilder, EntityConfig, EntityInfo}, generation::{BodyBuilder, EntityConfig, EntityInfo},
resources::{DeltaTime, Time, TimeOfDay}, resources::{DeltaTime, Time, TimeOfDay},
rtsim::{RtSimController, RtSimEntity}, rtsim::{RtSimController, RtSimEntity, RtSimVehicle},
slowjob::SlowJobPool, slowjob::SlowJobPool,
trade::{Good, SiteInformation}, trade::{Good, SiteInformation},
LoadoutBuilder, SkillSetBuilder, LoadoutBuilder, SkillSetBuilder,
}; };
use common_ecs::{Job, Origin, Phase, System}; use common_ecs::{Job, Origin, Phase, System};
use rtsim2::data::{ use rtsim2::data::{
npc::{NpcMode, Profession}, npc::{SimulationMode, Profession},
Npc, Sites, Npc, Sites, Actor,
}; };
use specs::{Join, Read, ReadExpect, ReadStorage, WriteExpect, WriteStorage}; use specs::{Join, Read, ReadExpect, ReadStorage, WriteExpect, WriteStorage};
use std::{sync::Arc, time::Duration}; use std::{sync::Arc, time::Duration};
@ -26,6 +26,7 @@ fn humanoid_config(profession: &Profession) -> &'static str {
Profession::Farmer => "common.entity.village.farmer", Profession::Farmer => "common.entity.village.farmer",
Profession::Hunter => "common.entity.village.hunter", Profession::Hunter => "common.entity.village.hunter",
Profession::Herbalist => "common.entity.village.herbalist", Profession::Herbalist => "common.entity.village.herbalist",
Profession::Captain => "common.entity.village.captain",
Profession::Merchant => "common.entity.village.merchant", Profession::Merchant => "common.entity.village.merchant",
Profession::Guard => "common.entity.village.guard", Profession::Guard => "common.entity.village.guard",
Profession::Adventurer(rank) => match rank { Profession::Adventurer(rank) => match rank {
@ -146,9 +147,9 @@ fn get_npc_entity_info(npc: &Npc, sites: &Sites, index: IndexRef) -> EntityInfo
} else { } else {
comp::Alignment::Npc comp::Alignment::Npc
}) })
.with_maybe_economy(economy.as_ref()) .with_economy(economy.as_ref())
.with_lazy_loadout(profession_extra_loadout(npc.profession.as_ref())) .with_lazy_loadout(profession_extra_loadout(npc.profession.as_ref()))
.with_maybe_agent_mark(profession_agent_mark(npc.profession.as_ref())) .with_agent_mark(profession_agent_mark(npc.profession.as_ref()))
} else { } else {
EntityInfo::at(pos.0) EntityInfo::at(pos.0)
.with_body(body) .with_body(body)
@ -171,6 +172,7 @@ impl<'a> System<'a> for Sys {
ReadExpect<'a, SlowJobPool>, ReadExpect<'a, SlowJobPool>,
ReadStorage<'a, comp::Pos>, ReadStorage<'a, comp::Pos>,
ReadStorage<'a, RtSimEntity>, ReadStorage<'a, RtSimEntity>,
ReadStorage<'a, RtSimVehicle>,
WriteStorage<'a, comp::Agent>, WriteStorage<'a, comp::Agent>,
); );
@ -191,6 +193,7 @@ impl<'a> System<'a> for Sys {
slow_jobs, slow_jobs,
positions, positions,
rtsim_entities, rtsim_entities,
rtsim_vehicles,
mut agents, mut agents,
): Self::SystemData, ): Self::SystemData,
) { ) {
@ -211,18 +214,79 @@ impl<'a> System<'a> for Sys {
let chunk_states = rtsim.state.resource::<ChunkStates>(); let chunk_states = rtsim.state.resource::<ChunkStates>();
let data = &mut *rtsim.state.data_mut(); let data = &mut *rtsim.state.data_mut();
for (npc_id, npc) in data.npcs.iter_mut() {
for (vehicle_id, vehicle) in data.npcs.vehicles.iter_mut() {
let chunk = vehicle.wpos.xy().map2(TerrainChunk::RECT_SIZE, |e, sz| {
(e as i32).div_euclid(sz as i32)
});
if matches!(vehicle.mode, SimulationMode::Simulated)
&& chunk_states.0.get(chunk).map_or(false, |c| c.is_some())
{
vehicle.mode = SimulationMode::Loaded;
let mut actor_info = |actor: Actor| {
let npc_id = actor.npc()?;
let npc = data.npcs.npcs.get_mut(npc_id)?;
if matches!(npc.mode, SimulationMode::Simulated) {
npc.mode = SimulationMode::Loaded;
let entity_info = get_npc_entity_info(npc, &data.sites, index.as_index_ref());
Some(match NpcData::from_entity_info(entity_info) {
NpcData::Data {
pos: _,
stats,
skill_set,
health,
poise,
inventory,
agent,
body,
alignment,
scale,
loot,
} => NpcBuilder::new(stats, body, alignment)
.with_skill_set(skill_set)
.with_health(health)
.with_poise(poise)
.with_inventory(inventory)
.with_agent(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!(),
})
} else {
error!("Npc is loaded but vehicle is unloaded");
None
}
};
emitter.emit(ServerEvent::CreateShip {
pos: comp::Pos(vehicle.wpos),
ship: vehicle.get_ship(),
// agent: None,//Some(Agent::from_body(&Body::Ship(ship))),
rtsim_entity: Some(RtSimVehicle(vehicle_id)),
driver: vehicle.driver.and_then(&mut actor_info),
passangers: vehicle.riders.iter().copied().filter(|actor| vehicle.driver != Some(*actor)).filter_map(actor_info).collect(),
});
}
}
for (npc_id, npc) in data.npcs.npcs.iter_mut() {
let chunk = npc.wpos.xy().map2(TerrainChunk::RECT_SIZE, |e, sz| { let chunk = npc.wpos.xy().map2(TerrainChunk::RECT_SIZE, |e, sz| {
(e as i32).div_euclid(sz as i32) (e as i32).div_euclid(sz as i32)
}); });
// Load the NPC into the world if it's in a loaded chunk and is not already // Load the NPC into the world if it's in a loaded chunk and is not already
// loaded // loaded
if matches!(npc.mode, NpcMode::Simulated) if matches!(npc.mode, SimulationMode::Simulated)
&& chunk_states.0.get(chunk).map_or(false, |c| c.is_some()) && chunk_states.0.get(chunk).map_or(false, |c| c.is_some())
{ {
npc.mode = NpcMode::Loaded; npc.mode = SimulationMode::Loaded;
let entity_info = get_npc_entity_info(npc, &data.sites, index.as_index_ref()); let entity_info = get_npc_entity_info(npc, &data.sites, index.as_index_ref());
emitter.emit(match NpcData::from_entity_info(entity_info) { emitter.emit(match NpcData::from_entity_info(entity_info) {
@ -240,19 +304,15 @@ impl<'a> System<'a> for Sys {
loot, loot,
} => ServerEvent::CreateNpc { } => ServerEvent::CreateNpc {
pos, pos,
stats, npc: NpcBuilder::new(stats, body, alignment)
skill_set, .with_skill_set(skill_set)
health, .with_health(health)
poise, .with_poise(poise)
inventory, .with_inventory(inventory)
agent, .with_agent(agent)
body, .with_scale(scale)
alignment, .with_loot(loot)
scale, .with_rtsim(RtSimEntity(npc_id)),
anchor: None,
loot,
rtsim_entity: Some(RtSimEntity(npc_id)),
projectile: None,
}, },
// EntityConfig can't represent Waypoints at all // EntityConfig can't represent Waypoints at all
// as of now, and if someone will try to spawn // as of now, and if someone will try to spawn
@ -262,21 +322,43 @@ impl<'a> System<'a> for Sys {
} }
} }
// 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;
});
}
// Synchronise rtsim NPC with entity data // Synchronise rtsim NPC with entity data
for (pos, rtsim_entity, agent) in for (pos, rtsim_entity, agent) in
(&positions, &rtsim_entities, (&mut agents).maybe()).join() (&positions, &rtsim_entities, (&mut agents).maybe()).join()
{ {
data.npcs data.npcs
.get_mut(rtsim_entity.0) .get_mut(rtsim_entity.0)
.filter(|npc| matches!(npc.mode, NpcMode::Loaded)) .filter(|npc| matches!(npc.mode, SimulationMode::Loaded))
.map(|npc| { .map(|npc| {
// Update rtsim NPC state // Update rtsim NPC state
npc.wpos = pos.0; npc.wpos = pos.0;
// Update entity state // Update entity state
if let Some(agent) = agent { if let Some(agent) = agent {
agent.rtsim_controller.travel_to = npc.goto.map(|(wpos, _)| wpos); if let Some(action) = npc.action {
agent.rtsim_controller.speed_factor = npc.goto.map_or(1.0, |(_, sf)| sf); match action {
rtsim2::data::npc::NpcAction::Goto(wpos, sf) => {
agent.rtsim_controller.travel_to = Some(wpos);
agent.rtsim_controller.speed_factor = sf;
},
}
} else {
agent.rtsim_controller.travel_to = None;
agent.rtsim_controller.speed_factor = 1.0;
}
} }
}); });
} }

View File

@ -65,7 +65,6 @@ pub trait StateExt {
pos: comp::Pos, pos: comp::Pos,
ship: comp::ship::Body, ship: comp::ship::Body,
make_collider: F, make_collider: F,
mountable: bool,
) -> EcsEntityBuilder; ) -> EcsEntityBuilder;
/// Build a projectile /// Build a projectile
fn create_projectile( fn create_projectile(
@ -338,7 +337,6 @@ impl StateExt for State {
pos: comp::Pos, pos: comp::Pos,
ship: comp::ship::Body, ship: comp::ship::Body,
make_collider: F, make_collider: F,
mountable: bool,
) -> EcsEntityBuilder { ) -> EcsEntityBuilder {
let body = comp::Body::Ship(ship); let body = comp::Body::Ship(ship);
let builder = self let builder = self
@ -362,9 +360,6 @@ impl StateExt for State {
.with(comp::ActiveAbilities::default()) .with(comp::ActiveAbilities::default())
.with(comp::Combo::default()); .with(comp::Combo::default());
if mountable {
// TODO: Re-add mounting check
}
builder builder
} }

View File

@ -1,5 +1,6 @@
pub mod behavior_tree; pub mod behavior_tree;
pub use server_agent::{action_nodes, attack, consts, data, util}; pub use server_agent::{action_nodes, attack, consts, data, util};
use vek::Vec3;
use crate::sys::agent::{ use crate::sys::agent::{
behavior_tree::{BehaviorData, BehaviorTree}, behavior_tree::{BehaviorData, BehaviorTree},
@ -18,7 +19,7 @@ use common_base::prof_span;
use common_ecs::{Job, Origin, ParMode, Phase, System}; use common_ecs::{Job, Origin, ParMode, Phase, System};
use rand::thread_rng; use rand::thread_rng;
use rayon::iter::ParallelIterator; use rayon::iter::ParallelIterator;
use specs::{Join, ParJoin, Read, WriteExpect, WriteStorage}; use specs::{Join, ParJoin, Read, WriteExpect, WriteStorage, saveload::MarkerAllocator};
/// This system will allow NPCs to modify their controller /// This system will allow NPCs to modify their controller
#[derive(Default)] #[derive(Default)]
@ -70,6 +71,7 @@ impl<'a> System<'a> for Sys {
read_data.groups.maybe(), read_data.groups.maybe(),
read_data.rtsim_entities.maybe(), read_data.rtsim_entities.maybe(),
!&read_data.is_mounts, !&read_data.is_mounts,
read_data.is_riders.maybe(),
) )
.par_join() .par_join()
.for_each_init( .for_each_init(
@ -93,10 +95,16 @@ impl<'a> System<'a> for Sys {
group, group,
rtsim_entity, rtsim_entity,
_, _,
is_rider,
)| { )| {
let mut event_emitter = event_bus.emitter(); let mut event_emitter = event_bus.emitter();
let mut rng = thread_rng(); let mut rng = thread_rng();
// The entity that is moving, if riding it's the mount, otherwise it's itself
let moving_entity = is_rider.and_then(|is_rider| read_data.uid_allocator.retrieve_entity_internal(is_rider.mount.into())).unwrap_or(entity);
let moving_body = read_data.bodies.get(moving_entity);
// Hack, replace with better system when groups are more sophisticated // Hack, replace with better system when groups are more sophisticated
// Override alignment if in a group unless entity is owned already // Override alignment if in a group unless entity is owned already
let alignment = if matches!( let alignment = if matches!(
@ -139,8 +147,17 @@ impl<'a> System<'a> for Sys {
Some(CharacterState::GlideWield(_) | CharacterState::Glide(_)) Some(CharacterState::GlideWield(_) | CharacterState::Glide(_))
) && physics_state.on_ground.is_none(); ) && physics_state.on_ground.is_none();
if let Some(pid) = agent.position_pid_controller.as_mut() { if let Some((kp, ki, kd)) = moving_body.and_then(comp::agent::pid_coefficients) {
if agent.position_pid_controller.as_ref().map_or(false, |pid| (pid.kp, pid.ki, pid.kd) != (kp, ki, kd)) {
agent.position_pid_controller = None;
}
let pid = agent.position_pid_controller.get_or_insert_with(|| {
fn pure_z(sp: Vec3<f32>, pv: Vec3<f32>) -> f32 { (sp - pv).z }
comp::PidController::new(kp, ki, kd, pos.0, 0.0, pure_z)
});
pid.add_measurement(read_data.time.0, pos.0); pid.add_measurement(read_data.time.0, pos.0);
} else {
agent.position_pid_controller = None;
} }
// This controls how picky NPCs are about their pathfinding. // This controls how picky NPCs are about their pathfinding.
@ -149,15 +166,15 @@ impl<'a> System<'a> for Sys {
// (especially since they would otherwise get stuck on // (especially since they would otherwise get stuck on
// obstacles that smaller entities would not). // obstacles that smaller entities would not).
let node_tolerance = scale * 1.5; let node_tolerance = scale * 1.5;
let slow_factor = body.map_or(0.0, |b| b.base_accel() / 250.0).min(1.0); let slow_factor = moving_body.map_or(0.0, |b| b.base_accel() / 250.0).min(1.0);
let traversal_config = TraversalConfig { let traversal_config = TraversalConfig {
node_tolerance, node_tolerance,
slow_factor, slow_factor,
on_ground: physics_state.on_ground.is_some(), on_ground: physics_state.on_ground.is_some(),
in_liquid: physics_state.in_liquid().is_some(), in_liquid: physics_state.in_liquid().is_some(),
min_tgt_dist: 1.0, min_tgt_dist: 1.0,
can_climb: body.map_or(false, Body::can_climb), can_climb: moving_body.map_or(false, Body::can_climb),
can_fly: body.map_or(false, |b| b.fly_thrust().is_some()), can_fly: moving_body.map_or(false, |b| b.fly_thrust().is_some()),
}; };
let health_fraction = health.map_or(1.0, Health::fraction); let health_fraction = health.map_or(1.0, Health::fraction);
/* /*
@ -167,7 +184,7 @@ impl<'a> System<'a> for Sys {
.and_then(|rtsim_ent| rtsim.get_entity(rtsim_ent.0)); .and_then(|rtsim_ent| rtsim.get_entity(rtsim_ent.0));
*/ */
if traversal_config.can_fly && matches!(body, Some(Body::Ship(_))) { if traversal_config.can_fly && matches!(moving_body, Some(Body::Ship(_))) {
// hack (kinda): Never turn off flight airships // hack (kinda): Never turn off flight airships
// since it results in stuttering and falling back to the ground. // since it results in stuttering and falling back to the ground.
// //

View File

@ -19,7 +19,7 @@ use common::{
comp::{ comp::{
self, agent, bird_medium, skillset::skills, BehaviorCapability, ForceUpdate, Pos, Waypoint, self, agent, bird_medium, skillset::skills, BehaviorCapability, ForceUpdate, Pos, Waypoint,
}, },
event::{EventBus, ServerEvent}, event::{EventBus, ServerEvent, NpcBuilder},
generation::EntityInfo, generation::EntityInfo,
lottery::LootSpec, lottery::LootSpec,
resources::{Time, TimeOfDay}, resources::{Time, TimeOfDay},
@ -217,19 +217,15 @@ impl<'a> System<'a> for Sys {
} => { } => {
server_emitter.emit(ServerEvent::CreateNpc { server_emitter.emit(ServerEvent::CreateNpc {
pos, pos,
stats, npc: NpcBuilder::new(stats, body, alignment)
skill_set, .with_skill_set(skill_set)
health, .with_health(health)
poise, .with_poise(poise)
inventory, .with_inventory(inventory)
agent, .with_agent(agent)
body, .with_scale(scale)
alignment, .with_anchor(comp::Anchor::Chunk(key))
scale, .with_loot(loot)
anchor: Some(comp::Anchor::Chunk(key)),
loot,
rtsim_entity: None,
projectile: None,
}); });
}, },
} }