rtsim vehicles

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

View File

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

View File

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

View File

@ -904,21 +904,20 @@ impl<F: Fn(Vec3<f32>, Vec3<f32>) -> f32, const NUM_SAMPLES: usize> PidController
/// Get the PID coefficients associated with some Body, since it will likely
/// 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,
}
}

View File

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

View File

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

View File

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

View File

@ -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(
comp::Agent::from_body(&body)
.with_behavior(Behavior::from(BehaviorCapability::SPEAK))
.with_no_flee_if(true),
),
alignment: comp::Alignment::Owned(*data.uid),
scale: self
.static_data
.summon_info
.scale
.unwrap_or(comp::Scale(1.0)),
anchor: None,
loot: crate::lottery::LootSpec::Nothing,
rtsim_entity: None,
projectile,
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)
)
.with_scale(
self
.static_data
.summon_info
.scale
.unwrap_or(comp::Scale(1.0))
).with_projectile(projectile)
});
// Send local event used for frontend shenanigans

View File

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

View File

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

View File

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

View File

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

View File

@ -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,30 +58,102 @@ 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());
// Simulate the NPC's movement and interactions
if matches!(npc.mode, NpcMode::Simulated) {
let body = npc.get_body();
// Move NPCs if they have a target destination
if let Some((target, speed_factor)) = npc.goto {
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);
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, SimulationMode::Simulated) {
let body = npc.get_body();
if let Some(riding) = &npc.riding {
if let Some(vehicle) = data.npcs.vehicles.get_mut(riding.vehicle) {
if let Some(action) = npc.action && riding.steering {
match action {
crate::data::npc::NpcAction::Goto(target, speed_factor) => {
let diff = target.xy() - vehicle.wpos.xy();
let dist2 = diff.magnitude_squared();
if dist2 > 0.5f32.powi(2) {
let mut wpos = vehicle.wpos + (diff
* (vehicle.get_speed() * speed_factor * ctx.event.dt
/ dist2.sqrt())
.min(1.0))
.with_z(0.0);
let is_valid = match vehicle.kind {
crate::data::npc::VehicleKind::Airship => true,
crate::data::npc::VehicleKind::Boat => {
let chunk_pos = wpos.xy().as_::<i32>() / TerrainChunkSize::RECT_SIZE.as_::<i32>();
ctx.world.sim().get(chunk_pos).map_or(true, |f| f.river.river_kind.is_some())
},
};
if is_valid {
match vehicle.kind {
crate::data::npc::VehicleKind::Airship => {
if let Some(alt) = ctx.world.sim().get_alt_approx(wpos.xy().as_()).filter(|alt| wpos.z < *alt) {
wpos.z = alt;
}
},
crate::data::npc::VehicleKind::Boat => {
wpos.z = ctx
.world
.sim()
.get_interpolated(wpos.xy().map(|e| e as i32), |chunk| chunk.water_alt)
.unwrap_or(0.0);
},
}
vehicle.wpos = wpos;
}
}
}
}
}
npc.wpos = vehicle.wpos;
} else {
// Vehicle doens't exist anymore
npc.riding = None;
}
}
// Move NPCs if they have a target destination
else if let Some(action) = npc.action {
match action {
crate::data::npc::NpcAction::Goto(target, speed_factor) => {
let diff = target.xy() - npc.wpos.xy();
let dist2 = diff.magnitude_squared();
if dist2 > 0.5f32.powi(2) {
npc.wpos += (diff
* (body.max_speed_approx() * speed_factor * ctx.event.dt
/ dist2.sqrt())
.min(1.0))
.with_z(0.0);
}
},
}
// Make sure NPCs remain on the surface
npc.wpos.z = ctx
.world
.sim()
.get_alt_approx(npc.wpos.xy().map(|e| e as i32))
.unwrap_or(0.0);
}
// Make sure NPCs remain on the surface
npc.wpos.z = ctx
.world
.sim()
.get_alt_approx(npc.wpos.xy().map(|e| e as i32))
.unwrap_or(0.0);
}
}
});

View File

@ -10,7 +10,7 @@ use common::{
Vel,
},
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")]

View File

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

View File

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

View File

@ -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(
self,
pos,
stats,
skill_set,
health,
poise,
inventory,
body,
agent,
alignment,
scale,
loot,
home_chunk,
rtsim_entity,
projectile,
),
npc,
} => {
handle_create_npc(
self,
pos,
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))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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