mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
rtsim vehicles
This commit is contained in:
parent
a7588e274d
commit
1a117f1331
19
assets/common/entity/village/captain.ron
Normal file
19
assets/common/entity/village/captain.ron
Normal 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: [],
|
||||
)
|
26
assets/common/loadout/village/captain.ron
Normal file
26
assets/common/loadout/village/captain.ron
Normal 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")),
|
||||
]),
|
||||
)
|
@ -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
|
||||
/// 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 {
|
||||
Body::Ship(ship::Body::DefaultAirship) => {
|
||||
let kp = 1.0;
|
||||
let ki = 0.1;
|
||||
let kd = 1.2;
|
||||
(kp, ki, kd)
|
||||
Some((kp, ki, kd))
|
||||
},
|
||||
Body::Ship(ship::Body::AirBalloon) => {
|
||||
let kp = 1.0;
|
||||
let ki = 0.1;
|
||||
let kd = 0.8;
|
||||
(kp, ki, kd)
|
||||
Some((kp, ki, kd))
|
||||
},
|
||||
// default to a pure-proportional controller, which is the first step when tuning
|
||||
_ => (1.0, 0.0, 0.0),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ use crate::{
|
||||
},
|
||||
lottery::LootSpec,
|
||||
outcome::Outcome,
|
||||
rtsim::RtSimEntity,
|
||||
rtsim::{RtSimEntity, RtSimVehicle},
|
||||
terrain::SpriteKind,
|
||||
trade::{TradeAction, TradeId},
|
||||
uid::Uid,
|
||||
@ -42,6 +42,83 @@ pub struct UpdateCharacterMetadata {
|
||||
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
|
||||
#[derive(strum::EnumDiscriminants)]
|
||||
#[strum_discriminants(repr(usize))]
|
||||
@ -137,26 +214,14 @@ pub enum ServerEvent {
|
||||
// TODO: to avoid breakage when adding new fields, perhaps have an `NpcBuilder` type?
|
||||
CreateNpc {
|
||||
pos: Pos,
|
||||
stats: comp::Stats,
|
||||
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>,
|
||||
npc: NpcBuilder,
|
||||
},
|
||||
CreateShip {
|
||||
pos: Pos,
|
||||
ship: comp::ship::Body,
|
||||
mountable: bool,
|
||||
agent: Option<comp::Agent>,
|
||||
rtsim_entity: Option<RtSimEntity>,
|
||||
rtsim_entity: Option<RtSimVehicle>,
|
||||
driver: Option<NpcBuilder>,
|
||||
passangers: Vec<NpcBuilder>,
|
||||
},
|
||||
CreateWaypoint(Vec3<f32>),
|
||||
ClientDisconnect(EcsEntity, DisconnectReason),
|
||||
|
@ -375,14 +375,8 @@ impl EntityInfo {
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_agent_mark(mut self, agent_mark: agent::Mark) -> Self {
|
||||
self.agent_mark = Some(agent_mark);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_maybe_agent_mark(mut self, agent_mark: Option<agent::Mark>) -> Self {
|
||||
self.agent_mark = agent_mark;
|
||||
pub fn with_agent_mark(mut self, agent_mark: impl Into<Option<agent::Mark>>) -> Self {
|
||||
self.agent_mark = agent_mark.into();
|
||||
self
|
||||
}
|
||||
|
||||
@ -442,15 +436,8 @@ impl EntityInfo {
|
||||
|
||||
/// map contains price+amount
|
||||
#[must_use]
|
||||
pub fn with_economy(mut self, e: &SiteInformation) -> Self {
|
||||
self.trading_information = Some(e.clone());
|
||||
self
|
||||
}
|
||||
|
||||
/// map contains price+amount
|
||||
#[must_use]
|
||||
pub fn with_maybe_economy(mut self, e: Option<&SiteInformation>) -> Self {
|
||||
self.trading_information = e.cloned();
|
||||
pub fn with_economy<'a>(mut self, e: impl Into<Option<&'a SiteInformation>>) -> Self {
|
||||
self.trading_information = e.into().cloned();
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -11,6 +11,8 @@ use crate::comp::dialogue::MoodState;
|
||||
|
||||
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; }
|
||||
@ -22,6 +24,13 @@ impl Component for RtSimEntity {
|
||||
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)]
|
||||
pub enum RtSimEvent {
|
||||
AddMemory(Memory),
|
||||
@ -139,6 +148,8 @@ pub enum Profession {
|
||||
Cultist,
|
||||
#[serde(rename = "10")]
|
||||
Herbalist,
|
||||
#[serde(rename = "11")]
|
||||
Captain,
|
||||
}
|
||||
|
||||
impl Profession {
|
||||
@ -155,6 +166,7 @@ impl Profession {
|
||||
Self::Pirate => "Pirate".to_string(),
|
||||
Self::Cultist => "Cultist".to_string(),
|
||||
Self::Herbalist => "Herbalist".to_string(),
|
||||
Self::Captain => "Captain".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ use crate::{
|
||||
skillset::skills,
|
||||
Behavior, BehaviorCapability, CharacterState, Projectile, StateUpdate,
|
||||
},
|
||||
event::{LocalEvent, ServerEvent},
|
||||
event::{LocalEvent, ServerEvent, NpcBuilder},
|
||||
outcome::Outcome,
|
||||
skillset_builder::{self, SkillSetBuilder},
|
||||
states::{
|
||||
@ -174,27 +174,22 @@ impl CharacterBehavior for Data {
|
||||
// Send server event to create npc
|
||||
output_events.emit_server(ServerEvent::CreateNpc {
|
||||
pos: comp::Pos(collision_vector - Vec3::unit_z() * obstacle_z),
|
||||
stats,
|
||||
skill_set,
|
||||
health,
|
||||
poise: comp::Poise::new(body),
|
||||
inventory: comp::Inventory::with_loadout(loadout, body),
|
||||
body,
|
||||
agent: Some(
|
||||
npc: NpcBuilder::new(stats, body, comp::Alignment::Owned(*data.uid))
|
||||
.with_skill_set(skill_set)
|
||||
.with_health(health)
|
||||
.with_inventory(comp::Inventory::with_loadout(loadout, body))
|
||||
.with_agent(
|
||||
comp::Agent::from_body(&body)
|
||||
.with_behavior(Behavior::from(BehaviorCapability::SPEAK))
|
||||
.with_no_flee_if(true),
|
||||
),
|
||||
alignment: comp::Alignment::Owned(*data.uid),
|
||||
scale: self
|
||||
.with_no_flee_if(true)
|
||||
)
|
||||
.with_scale(
|
||||
self
|
||||
.static_data
|
||||
.summon_info
|
||||
.scale
|
||||
.unwrap_or(comp::Scale(1.0)),
|
||||
anchor: None,
|
||||
loot: crate::lottery::LootSpec::Nothing,
|
||||
rtsim_entity: None,
|
||||
projectile,
|
||||
.unwrap_or(comp::Scale(1.0))
|
||||
).with_projectile(projectile)
|
||||
});
|
||||
|
||||
// Send local event used for frontend shenanigans
|
||||
|
@ -23,12 +23,21 @@ use std::{
|
||||
marker::PhantomData,
|
||||
};
|
||||
|
||||
#[derive(Copy, Clone, Serialize, Deserialize)]
|
||||
#[derive(Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum Actor {
|
||||
Npc(NpcId),
|
||||
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)]
|
||||
pub struct Data {
|
||||
pub nature: Nature,
|
||||
|
@ -2,9 +2,10 @@ use crate::ai::{Action, NpcCtx};
|
||||
pub use common::rtsim::{NpcId, Profession};
|
||||
use common::{
|
||||
comp,
|
||||
rtsim::{FactionId, RtSimController, SiteId},
|
||||
grid::Grid,
|
||||
rtsim::{FactionId, RtSimController, SiteId, VehicleId},
|
||||
store::Id,
|
||||
uid::Uid,
|
||||
uid::Uid, vol::RectVolSize,
|
||||
};
|
||||
use hashbrown::HashMap;
|
||||
use rand::prelude::*;
|
||||
@ -20,10 +21,12 @@ use std::{
|
||||
},
|
||||
};
|
||||
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)]
|
||||
pub enum NpcMode {
|
||||
pub enum SimulationMode {
|
||||
/// The NPC is unloaded and is being simulated via rtsim.
|
||||
#[default]
|
||||
Simulated,
|
||||
@ -44,12 +47,24 @@ pub struct PathingMemory {
|
||||
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 goto: Option<(Vec3<f32>, f32)>,
|
||||
pub action: Option<NpcAction>,
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -67,20 +82,23 @@ pub struct Npc {
|
||||
pub home: Option<SiteId>,
|
||||
pub faction: Option<FactionId>,
|
||||
|
||||
pub riding: Option<Riding>,
|
||||
|
||||
// Unpersisted state
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
pub chunk_pos: Option<Vec2<i32>>,
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
pub current_site: Option<SiteId>,
|
||||
|
||||
/// (wpos, speed_factor)
|
||||
#[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
|
||||
/// server, loaded corresponds to being within a loaded chunk). When in
|
||||
/// loaded mode, the interactions of the NPC should not be simulated but
|
||||
/// should instead be derived from the game.
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
pub mode: NpcMode,
|
||||
pub mode: SimulationMode,
|
||||
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
pub brain: Option<Brain>,
|
||||
@ -94,9 +112,11 @@ impl Clone for Npc {
|
||||
profession: self.profession.clone(),
|
||||
home: self.home,
|
||||
faction: self.faction,
|
||||
riding: self.riding.clone(),
|
||||
// Not persisted
|
||||
chunk_pos: None,
|
||||
current_site: Default::default(),
|
||||
goto: Default::default(),
|
||||
action: Default::default(),
|
||||
mode: Default::default(),
|
||||
brain: Default::default(),
|
||||
}
|
||||
@ -114,9 +134,11 @@ impl Npc {
|
||||
profession: None,
|
||||
home: None,
|
||||
faction: None,
|
||||
riding: None,
|
||||
chunk_pos: None,
|
||||
current_site: None,
|
||||
goto: None,
|
||||
mode: NpcMode::Simulated,
|
||||
action: None,
|
||||
mode: SimulationMode::Simulated,
|
||||
brain: None,
|
||||
}
|
||||
}
|
||||
@ -131,6 +153,26 @@ impl Npc {
|
||||
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 {
|
||||
self.faction = faction.into();
|
||||
self
|
||||
@ -147,12 +189,115 @@ impl Npc {
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct Npcs {
|
||||
pub npcs: HopSlotMap<NpcId, Npc>,
|
||||
pub struct Riding {
|
||||
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 {
|
||||
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 {
|
||||
|
@ -3,12 +3,13 @@ pub mod site;
|
||||
|
||||
use crate::data::{
|
||||
faction::{Faction, Factions},
|
||||
npc::{Npc, Npcs, Profession},
|
||||
npc::{Npc, Npcs, Profession, Vehicle, VehicleKind},
|
||||
site::{Site, Sites},
|
||||
Data, Nature,
|
||||
};
|
||||
use common::{
|
||||
resources::TimeOfDay, rtsim::WorldSettings, terrain::TerrainChunkSize, vol::RectVolSize,
|
||||
grid::Grid, resources::TimeOfDay, rtsim::WorldSettings, terrain::TerrainChunkSize,
|
||||
vol::RectVolSize,
|
||||
};
|
||||
use hashbrown::HashMap;
|
||||
use rand::prelude::*;
|
||||
@ -28,6 +29,8 @@ impl Data {
|
||||
nature: Nature::generate(world),
|
||||
npcs: Npcs {
|
||||
npcs: Default::default(),
|
||||
vehicles: Default::default(),
|
||||
npc_grid: Grid::new(Vec2::zero(), Default::default()),
|
||||
},
|
||||
sites: Sites {
|
||||
sites: Default::default(),
|
||||
@ -75,7 +78,6 @@ impl Data {
|
||||
// TODO: Stupid
|
||||
.filter(|(_, site)| site.world_site.map_or(false, |ws|
|
||||
matches!(&index.sites.get(ws).kind, SiteKind::Refactor(_)))) .skip(1)
|
||||
.take(1)
|
||||
{
|
||||
let Some(good_or_evil) = site
|
||||
.faction
|
||||
@ -91,7 +93,7 @@ impl Data {
|
||||
};
|
||||
if good_or_evil {
|
||||
for _ in 0..32 {
|
||||
this.npcs.create(
|
||||
this.npcs.create_npc(
|
||||
Npc::new(rng.gen(), rand_wpos(&mut rng))
|
||||
.with_faction(site.faction)
|
||||
.with_home(site_id)
|
||||
@ -109,7 +111,7 @@ impl Data {
|
||||
}
|
||||
} else {
|
||||
for _ in 0..15 {
|
||||
this.npcs.create(
|
||||
this.npcs.create_npc(
|
||||
Npc::new(rng.gen(), rand_wpos(&mut rng))
|
||||
.with_faction(site.faction)
|
||||
.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))
|
||||
.with_home(site_id)
|
||||
.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());
|
||||
|
||||
this
|
||||
|
@ -3,7 +3,7 @@ use std::{collections::VecDeque, hash::BuildHasherDefault};
|
||||
use crate::{
|
||||
ai::{casual, choose, finish, important, just, now, seq, until, urgent, watch, Action, NpcCtx},
|
||||
data::{
|
||||
npc::{Brain, Controller, Npc, NpcId, PathData, PathingMemory},
|
||||
npc::{Brain, Controller, Npc, NpcId, PathData, PathingMemory, VehicleKind},
|
||||
Sites,
|
||||
},
|
||||
event::OnTick,
|
||||
@ -14,7 +14,7 @@ use common::{
|
||||
path::Path,
|
||||
rtsim::{Profession, SiteId},
|
||||
store::Id,
|
||||
terrain::TerrainChunkSize,
|
||||
terrain::{TerrainChunkSize, SiteKindMeta},
|
||||
time::DayPeriod,
|
||||
vol::RectVolSize,
|
||||
};
|
||||
@ -32,7 +32,7 @@ use world::{
|
||||
civ::{self, Track},
|
||||
site::{Site as WorldSite, SiteKind},
|
||||
site2::{self, PlotKind, TileKind},
|
||||
IndexRef, World,
|
||||
IndexRef, World, util::NEIGHBORS,
|
||||
};
|
||||
|
||||
pub struct NpcAi;
|
||||
@ -221,7 +221,7 @@ impl Rule for NpcAi {
|
||||
data.npcs
|
||||
.iter_mut()
|
||||
.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 {
|
||||
action: Box::new(think().repeat()),
|
||||
});
|
||||
@ -253,7 +253,7 @@ impl Rule for NpcAi {
|
||||
|
||||
let mut data = ctx.state.data_mut();
|
||||
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);
|
||||
}
|
||||
|
||||
@ -331,7 +331,7 @@ fn goto(wpos: Vec3<f32>, speed_factor: f32, goal_dist: f32) -> impl Action {
|
||||
let waypoint =
|
||||
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()
|
||||
.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))
|
||||
}
|
||||
|
||||
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 {
|
||||
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,
|
||||
Some(Profession::Adventurer(_) | Profession::Merchant)
|
||||
) {
|
||||
|
@ -1,5 +1,9 @@
|
||||
use crate::{data::npc::NpcMode, event::OnTick, RtState, Rule, RuleError};
|
||||
use common::{terrain::TerrainChunkSize, vol::RectVolSize};
|
||||
use crate::{
|
||||
data::npc::SimulationMode,
|
||||
event::{OnSetup, OnTick},
|
||||
RtState, Rule, RuleError,
|
||||
};
|
||||
use common::{terrain::TerrainChunkSize, vol::RectVolSize, grid::Grid};
|
||||
use tracing::info;
|
||||
use vek::*;
|
||||
|
||||
@ -7,9 +11,46 @@ pub struct SimulateNpcs;
|
||||
|
||||
impl Rule for SimulateNpcs {
|
||||
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| {
|
||||
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
|
||||
npc.current_site = ctx
|
||||
.world
|
||||
@ -17,12 +58,81 @@ impl Rule for SimulateNpcs {
|
||||
.get(npc.wpos.xy().as_::<i32>() / TerrainChunkSize::RECT_SIZE.as_())
|
||||
.and_then(|chunk| data.sites.world_site_map.get(chunk.sites.first()?).copied());
|
||||
|
||||
let chunk_pos =
|
||||
npc.wpos.xy().as_::<i32>() / TerrainChunkSize::RECT_SIZE.as_::<i32>();
|
||||
if npc.chunk_pos != Some(chunk_pos) {
|
||||
if let Some(cell) = npc
|
||||
.chunk_pos
|
||||
.and_then(|chunk_pos| data.npcs.npc_grid.get_mut(chunk_pos))
|
||||
{
|
||||
if let Some(index) = cell.npcs.iter().position(|id| *id == npc_id) {
|
||||
cell.npcs.swap_remove(index);
|
||||
}
|
||||
}
|
||||
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, NpcMode::Simulated) {
|
||||
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
|
||||
if let Some((target, speed_factor)) = npc.goto {
|
||||
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();
|
||||
|
||||
@ -33,6 +143,7 @@ impl Rule for SimulateNpcs {
|
||||
.min(1.0))
|
||||
.with_z(0.0);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// Make sure NPCs remain on the surface
|
||||
@ -42,6 +153,8 @@ impl Rule for SimulateNpcs {
|
||||
.get_alt_approx(npc.wpos.xy().map(|e| e as i32))
|
||||
.unwrap_or(0.0);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -10,7 +10,7 @@ use common::{
|
||||
Vel,
|
||||
},
|
||||
link::Is,
|
||||
mounting::Mount,
|
||||
mounting::{Mount, Rider},
|
||||
path::TraversalConfig,
|
||||
resources::{DeltaTime, Time, TimeOfDay},
|
||||
rtsim::RtSimEntity,
|
||||
@ -233,6 +233,7 @@ pub struct ReadData<'a> {
|
||||
pub alignments: ReadStorage<'a, Alignment>,
|
||||
pub bodies: ReadStorage<'a, Body>,
|
||||
pub is_mounts: ReadStorage<'a, Is<Mount>>,
|
||||
pub is_riders: ReadStorage<'a, Is<Rider>>,
|
||||
pub time_of_day: Read<'a, TimeOfDay>,
|
||||
pub light_emitter: ReadStorage<'a, LightEmitter>,
|
||||
#[cfg(feature = "worldgen")]
|
||||
|
@ -1480,7 +1480,7 @@ fn handle_spawn_airship(
|
||||
let ship = comp::ship::Body::random_airship_with(&mut rng);
|
||||
let mut builder = server
|
||||
.state
|
||||
.create_ship(pos, ship, |ship| ship.make_collider(), true)
|
||||
.create_ship(pos, ship, |ship| ship.make_collider())
|
||||
.with(LightEmitter {
|
||||
col: Rgb::new(1.0, 0.65, 0.2),
|
||||
strength: 2.0,
|
||||
@ -1488,7 +1488,7 @@ fn handle_spawn_airship(
|
||||
animated: true,
|
||||
});
|
||||
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 }
|
||||
let agent = comp::Agent::from_body(&comp::Body::Ship(ship))
|
||||
.with_destination(pos)
|
||||
@ -1528,7 +1528,7 @@ fn handle_spawn_ship(
|
||||
let ship = comp::ship::Body::random_ship_with(&mut rng);
|
||||
let mut builder = server
|
||||
.state
|
||||
.create_ship(pos, ship, |ship| ship.make_collider(), true)
|
||||
.create_ship(pos, ship, |ship| ship.make_collider())
|
||||
.with(LightEmitter {
|
||||
col: Rgb::new(1.0, 0.65, 0.2),
|
||||
strength: 2.0,
|
||||
@ -1536,7 +1536,7 @@ fn handle_spawn_ship(
|
||||
animated: true,
|
||||
});
|
||||
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 }
|
||||
let agent = comp::Agent::from_body(&comp::Body::Ship(ship))
|
||||
.with_destination(pos)
|
||||
@ -1581,7 +1581,6 @@ fn handle_make_volume(
|
||||
comp::Pos(pos.0 + Vec3::unit_z() * 50.0),
|
||||
ship,
|
||||
move |_| collider,
|
||||
true,
|
||||
)
|
||||
.build();
|
||||
|
||||
|
@ -14,14 +14,14 @@ use common::{
|
||||
LightEmitter, Object, Ori, PidController, Poise, Pos, Projectile, Scale, SkillSet, Stats,
|
||||
TradingBehavior, Vel, WaypointArea,
|
||||
},
|
||||
event::{EventBus, UpdateCharacterMetadata},
|
||||
event::{EventBus, UpdateCharacterMetadata, NpcBuilder},
|
||||
lottery::LootSpec,
|
||||
outcome::Outcome,
|
||||
resources::{Secs, Time},
|
||||
rtsim::RtSimEntity,
|
||||
rtsim::{RtSimEntity, RtSimVehicle},
|
||||
uid::Uid,
|
||||
util::Dir,
|
||||
ViewDistances,
|
||||
ViewDistances, mounting::Mounting,
|
||||
};
|
||||
use common_net::{msg::ServerGeneral, sync::WorldSyncExt};
|
||||
use specs::{Builder, Entity as EcsEntity, WorldExt};
|
||||
@ -94,60 +94,47 @@ pub fn handle_loaded_character_data(
|
||||
pub fn handle_create_npc(
|
||||
server: &mut Server,
|
||||
pos: Pos,
|
||||
stats: Stats,
|
||||
skill_set: SkillSet,
|
||||
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>,
|
||||
) {
|
||||
mut npc: NpcBuilder,
|
||||
) -> EcsEntity {
|
||||
let entity = server
|
||||
.state
|
||||
.create_npc(pos, stats, skill_set, health, poise, inventory, body)
|
||||
.with(scale);
|
||||
.create_npc(pos, npc.stats, npc.skill_set, npc.health, npc.poise, npc.inventory, npc.body)
|
||||
.with(npc.scale);
|
||||
|
||||
let mut agent = agent.into();
|
||||
if let Some(agent) = &mut agent {
|
||||
if let Alignment::Owned(_) = &alignment {
|
||||
if let Some(agent) = &mut npc.agent {
|
||||
if let Alignment::Owned(_) = &npc.alignment {
|
||||
agent.behavior.allow(BehaviorCapability::TRADE);
|
||||
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)
|
||||
} else {
|
||||
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))
|
||||
} else {
|
||||
entity
|
||||
};
|
||||
|
||||
let entity = if let Some(home_chunk) = home_chunk {
|
||||
let entity = if let Some(home_chunk) = npc.anchor {
|
||||
entity.with(home_chunk)
|
||||
} else {
|
||||
entity
|
||||
};
|
||||
|
||||
let entity = if let Some(rtsim_entity) = rtsim_entity {
|
||||
let entity = if let Some(rtsim_entity) = npc.rtsim_entity {
|
||||
entity.with(rtsim_entity)
|
||||
} else {
|
||||
entity
|
||||
};
|
||||
|
||||
let entity = if let Some(projectile) = projectile {
|
||||
let entity = if let Some(projectile) = npc.projectile {
|
||||
entity.with(projectile)
|
||||
} else {
|
||||
entity
|
||||
@ -156,7 +143,7 @@ pub fn handle_create_npc(
|
||||
let new_entity = entity.build();
|
||||
|
||||
// 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 clients = state.ecs().read_storage::<Client>();
|
||||
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::Passive => None,
|
||||
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);
|
||||
}
|
||||
|
||||
new_entity
|
||||
}
|
||||
|
||||
pub fn handle_create_ship(
|
||||
server: &mut Server,
|
||||
pos: Pos,
|
||||
ship: comp::ship::Body,
|
||||
mountable: bool,
|
||||
agent: Option<Agent>,
|
||||
rtsim_entity: Option<RtSimEntity>,
|
||||
rtsim_vehicle: Option<RtSimVehicle>,
|
||||
driver: Option<NpcBuilder>,
|
||||
passangers: Vec<NpcBuilder>,
|
||||
|
||||
) {
|
||||
let mut entity = server
|
||||
.state
|
||||
.create_ship(pos, ship, |ship| ship.make_collider(), mountable);
|
||||
.create_ship(pos, ship, |ship| ship.make_collider());
|
||||
/*
|
||||
if let Some(mut agent) = agent {
|
||||
let (kp, ki, kd) = pid_coefficients(&Body::Ship(ship));
|
||||
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));
|
||||
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(
|
||||
|
@ -190,43 +190,21 @@ impl Server {
|
||||
},
|
||||
ServerEvent::CreateNpc {
|
||||
pos,
|
||||
stats,
|
||||
skill_set,
|
||||
health,
|
||||
poise,
|
||||
inventory,
|
||||
body,
|
||||
agent,
|
||||
alignment,
|
||||
scale,
|
||||
anchor: home_chunk,
|
||||
loot,
|
||||
rtsim_entity,
|
||||
projectile,
|
||||
} => handle_create_npc(
|
||||
npc,
|
||||
} => {
|
||||
handle_create_npc(
|
||||
self,
|
||||
pos,
|
||||
stats,
|
||||
skill_set,
|
||||
health,
|
||||
poise,
|
||||
inventory,
|
||||
body,
|
||||
agent,
|
||||
alignment,
|
||||
scale,
|
||||
loot,
|
||||
home_chunk,
|
||||
rtsim_entity,
|
||||
projectile,
|
||||
),
|
||||
npc,
|
||||
);
|
||||
},
|
||||
ServerEvent::CreateShip {
|
||||
pos,
|
||||
ship,
|
||||
mountable,
|
||||
agent,
|
||||
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::ClientDisconnect(entity, reason) => {
|
||||
frontend_events.push(handle_client_disconnect(self, entity, reason, false))
|
||||
|
@ -80,8 +80,8 @@ use common::{
|
||||
comp,
|
||||
event::{EventBus, ServerEvent},
|
||||
resources::{BattleMode, GameMode, Time, TimeOfDay},
|
||||
rtsim::RtSimEntity,
|
||||
shared_server_config::ServerConstants,
|
||||
rtsim::{RtSimEntity, RtSimVehicle},
|
||||
slowjob::SlowJobPool,
|
||||
terrain::{Block, TerrainChunk, TerrainChunkSize},
|
||||
vol::RectRasterableVol,
|
||||
@ -387,6 +387,7 @@ 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);
|
||||
@ -873,6 +874,19 @@ impl Server {
|
||||
.write_resource::<rtsim2::RtSim>()
|
||||
.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) {
|
||||
error!(?e, "Failed to delete agent outside the terrain");
|
||||
|
@ -4,7 +4,7 @@ pub mod tick;
|
||||
|
||||
use common::{
|
||||
grid::Grid,
|
||||
rtsim::{ChunkResource, RtSimEntity, WorldSettings},
|
||||
rtsim::{ChunkResource, RtSimEntity, WorldSettings, RtSimVehicle},
|
||||
slowjob::SlowJobPool,
|
||||
terrain::{Block, TerrainChunk},
|
||||
vol::RectRasterableVol,
|
||||
@ -12,7 +12,7 @@ use common::{
|
||||
use common_ecs::{dispatch, System};
|
||||
use enum_map::EnumMap;
|
||||
use rtsim2::{
|
||||
data::{npc::NpcMode, Data, ReadError},
|
||||
data::{npc::SimulationMode, Data, ReadError},
|
||||
event::{OnDeath, OnSetup},
|
||||
rule::Rule,
|
||||
RtState,
|
||||
@ -150,7 +150,13 @@ impl RtSim {
|
||||
|
||||
pub fn hook_rtsim_entity_unload(&mut self, entity: RtSimEntity) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,19 +3,19 @@
|
||||
use super::*;
|
||||
use crate::sys::terrain::NpcData;
|
||||
use common::{
|
||||
comp::{self, inventory::loadout::Loadout, skillset::skills},
|
||||
event::{EventBus, ServerEvent},
|
||||
comp::{self, inventory::loadout::Loadout, skillset::skills, Body, Agent},
|
||||
event::{EventBus, ServerEvent, NpcBuilder},
|
||||
generation::{BodyBuilder, EntityConfig, EntityInfo},
|
||||
resources::{DeltaTime, Time, TimeOfDay},
|
||||
rtsim::{RtSimController, RtSimEntity},
|
||||
rtsim::{RtSimController, RtSimEntity, RtSimVehicle},
|
||||
slowjob::SlowJobPool,
|
||||
trade::{Good, SiteInformation},
|
||||
LoadoutBuilder, SkillSetBuilder,
|
||||
};
|
||||
use common_ecs::{Job, Origin, Phase, System};
|
||||
use rtsim2::data::{
|
||||
npc::{NpcMode, Profession},
|
||||
Npc, Sites,
|
||||
npc::{SimulationMode, Profession},
|
||||
Npc, Sites, Actor,
|
||||
};
|
||||
use specs::{Join, Read, ReadExpect, ReadStorage, WriteExpect, WriteStorage};
|
||||
use std::{sync::Arc, time::Duration};
|
||||
@ -26,6 +26,7 @@ fn humanoid_config(profession: &Profession) -> &'static str {
|
||||
Profession::Farmer => "common.entity.village.farmer",
|
||||
Profession::Hunter => "common.entity.village.hunter",
|
||||
Profession::Herbalist => "common.entity.village.herbalist",
|
||||
Profession::Captain => "common.entity.village.captain",
|
||||
Profession::Merchant => "common.entity.village.merchant",
|
||||
Profession::Guard => "common.entity.village.guard",
|
||||
Profession::Adventurer(rank) => match rank {
|
||||
@ -146,9 +147,9 @@ fn get_npc_entity_info(npc: &Npc, sites: &Sites, index: IndexRef) -> EntityInfo
|
||||
} else {
|
||||
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_maybe_agent_mark(profession_agent_mark(npc.profession.as_ref()))
|
||||
.with_agent_mark(profession_agent_mark(npc.profession.as_ref()))
|
||||
} else {
|
||||
EntityInfo::at(pos.0)
|
||||
.with_body(body)
|
||||
@ -171,6 +172,7 @@ impl<'a> System<'a> for Sys {
|
||||
ReadExpect<'a, SlowJobPool>,
|
||||
ReadStorage<'a, comp::Pos>,
|
||||
ReadStorage<'a, RtSimEntity>,
|
||||
ReadStorage<'a, RtSimVehicle>,
|
||||
WriteStorage<'a, comp::Agent>,
|
||||
);
|
||||
|
||||
@ -191,6 +193,7 @@ impl<'a> System<'a> for Sys {
|
||||
slow_jobs,
|
||||
positions,
|
||||
rtsim_entities,
|
||||
rtsim_vehicles,
|
||||
mut agents,
|
||||
): Self::SystemData,
|
||||
) {
|
||||
@ -211,18 +214,79 @@ impl<'a> System<'a> for Sys {
|
||||
|
||||
let chunk_states = rtsim.state.resource::<ChunkStates>();
|
||||
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| {
|
||||
(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
|
||||
// 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())
|
||||
{
|
||||
npc.mode = NpcMode::Loaded;
|
||||
|
||||
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) {
|
||||
@ -240,19 +304,15 @@ impl<'a> System<'a> for Sys {
|
||||
loot,
|
||||
} => ServerEvent::CreateNpc {
|
||||
pos,
|
||||
stats,
|
||||
skill_set,
|
||||
health,
|
||||
poise,
|
||||
inventory,
|
||||
agent,
|
||||
body,
|
||||
alignment,
|
||||
scale,
|
||||
anchor: None,
|
||||
loot,
|
||||
rtsim_entity: Some(RtSimEntity(npc_id)),
|
||||
projectile: None,
|
||||
npc: 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
|
||||
@ -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
|
||||
for (pos, rtsim_entity, agent) in
|
||||
(&positions, &rtsim_entities, (&mut agents).maybe()).join()
|
||||
{
|
||||
data.npcs
|
||||
.get_mut(rtsim_entity.0)
|
||||
.filter(|npc| matches!(npc.mode, NpcMode::Loaded))
|
||||
.filter(|npc| matches!(npc.mode, SimulationMode::Loaded))
|
||||
.map(|npc| {
|
||||
// Update rtsim NPC state
|
||||
npc.wpos = pos.0;
|
||||
|
||||
// Update entity state
|
||||
if let Some(agent) = agent {
|
||||
agent.rtsim_controller.travel_to = npc.goto.map(|(wpos, _)| wpos);
|
||||
agent.rtsim_controller.speed_factor = npc.goto.map_or(1.0, |(_, sf)| sf);
|
||||
if let Some(action) = npc.action {
|
||||
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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -65,7 +65,6 @@ pub trait StateExt {
|
||||
pos: comp::Pos,
|
||||
ship: comp::ship::Body,
|
||||
make_collider: F,
|
||||
mountable: bool,
|
||||
) -> EcsEntityBuilder;
|
||||
/// Build a projectile
|
||||
fn create_projectile(
|
||||
@ -338,7 +337,6 @@ impl StateExt for State {
|
||||
pos: comp::Pos,
|
||||
ship: comp::ship::Body,
|
||||
make_collider: F,
|
||||
mountable: bool,
|
||||
) -> EcsEntityBuilder {
|
||||
let body = comp::Body::Ship(ship);
|
||||
let builder = self
|
||||
@ -362,9 +360,6 @@ impl StateExt for State {
|
||||
.with(comp::ActiveAbilities::default())
|
||||
.with(comp::Combo::default());
|
||||
|
||||
if mountable {
|
||||
// TODO: Re-add mounting check
|
||||
}
|
||||
builder
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
pub mod behavior_tree;
|
||||
pub use server_agent::{action_nodes, attack, consts, data, util};
|
||||
use vek::Vec3;
|
||||
|
||||
use crate::sys::agent::{
|
||||
behavior_tree::{BehaviorData, BehaviorTree},
|
||||
@ -18,7 +19,7 @@ use common_base::prof_span;
|
||||
use common_ecs::{Job, Origin, ParMode, Phase, System};
|
||||
use rand::thread_rng;
|
||||
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
|
||||
#[derive(Default)]
|
||||
@ -70,6 +71,7 @@ impl<'a> System<'a> for Sys {
|
||||
read_data.groups.maybe(),
|
||||
read_data.rtsim_entities.maybe(),
|
||||
!&read_data.is_mounts,
|
||||
read_data.is_riders.maybe(),
|
||||
)
|
||||
.par_join()
|
||||
.for_each_init(
|
||||
@ -93,10 +95,16 @@ impl<'a> System<'a> for Sys {
|
||||
group,
|
||||
rtsim_entity,
|
||||
_,
|
||||
is_rider,
|
||||
)| {
|
||||
let mut event_emitter = event_bus.emitter();
|
||||
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
|
||||
// Override alignment if in a group unless entity is owned already
|
||||
let alignment = if matches!(
|
||||
@ -139,8 +147,17 @@ impl<'a> System<'a> for Sys {
|
||||
Some(CharacterState::GlideWield(_) | CharacterState::Glide(_))
|
||||
) && 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);
|
||||
} else {
|
||||
agent.position_pid_controller = None;
|
||||
}
|
||||
|
||||
// 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
|
||||
// obstacles that smaller entities would not).
|
||||
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 {
|
||||
node_tolerance,
|
||||
slow_factor,
|
||||
on_ground: physics_state.on_ground.is_some(),
|
||||
in_liquid: physics_state.in_liquid().is_some(),
|
||||
min_tgt_dist: 1.0,
|
||||
can_climb: body.map_or(false, Body::can_climb),
|
||||
can_fly: body.map_or(false, |b| b.fly_thrust().is_some()),
|
||||
can_climb: moving_body.map_or(false, Body::can_climb),
|
||||
can_fly: moving_body.map_or(false, |b| b.fly_thrust().is_some()),
|
||||
};
|
||||
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));
|
||||
*/
|
||||
|
||||
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
|
||||
// since it results in stuttering and falling back to the ground.
|
||||
//
|
||||
|
@ -19,7 +19,7 @@ use common::{
|
||||
comp::{
|
||||
self, agent, bird_medium, skillset::skills, BehaviorCapability, ForceUpdate, Pos, Waypoint,
|
||||
},
|
||||
event::{EventBus, ServerEvent},
|
||||
event::{EventBus, ServerEvent, NpcBuilder},
|
||||
generation::EntityInfo,
|
||||
lottery::LootSpec,
|
||||
resources::{Time, TimeOfDay},
|
||||
@ -217,19 +217,15 @@ impl<'a> System<'a> for Sys {
|
||||
} => {
|
||||
server_emitter.emit(ServerEvent::CreateNpc {
|
||||
pos,
|
||||
stats,
|
||||
skill_set,
|
||||
health,
|
||||
poise,
|
||||
inventory,
|
||||
agent,
|
||||
body,
|
||||
alignment,
|
||||
scale,
|
||||
anchor: Some(comp::Anchor::Chunk(key)),
|
||||
loot,
|
||||
rtsim_entity: None,
|
||||
projectile: None,
|
||||
npc: 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_anchor(comp::Anchor::Chunk(key))
|
||||
.with_loot(loot)
|
||||
});
|
||||
},
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user