mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
rtsim vehicles
This commit is contained in:
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
|
/// 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),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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),
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
) {
|
) {
|
||||||
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -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")]
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
@ -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(
|
||||||
|
@ -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))
|
||||||
|
@ -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");
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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.
|
||||||
//
|
//
|
||||||
|
@ -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,
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user