mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Cleaned up rtsim rules
This commit is contained in:
parent
3e0f5295c0
commit
5614eaa7a5
@ -296,6 +296,11 @@ pub enum ServerChatCommand {
|
||||
Respawn,
|
||||
RevokeBuild,
|
||||
RevokeBuildAll,
|
||||
RtsimChunk,
|
||||
RtsimInfo,
|
||||
RtsimNpc,
|
||||
RtsimPurge,
|
||||
RtsimTp,
|
||||
Safezone,
|
||||
Say,
|
||||
Scale,
|
||||
@ -310,11 +315,6 @@ pub enum ServerChatCommand {
|
||||
Tell,
|
||||
Time,
|
||||
Tp,
|
||||
RtsimChunk,
|
||||
RtsimInfo,
|
||||
RtsimNpc,
|
||||
RtsimPurge,
|
||||
RtsimTp,
|
||||
Unban,
|
||||
Version,
|
||||
Waypoint,
|
||||
|
@ -60,8 +60,9 @@ impl RtState {
|
||||
|
||||
fn start_default_rules(&mut self) {
|
||||
info!("Starting default rtsim rules...");
|
||||
self.start_rule::<rule::setup::Setup>();
|
||||
self.start_rule::<rule::migrate::Migrate>();
|
||||
self.start_rule::<rule::replenish_resources::ReplenishResources>();
|
||||
self.start_rule::<rule::sync_npcs::SyncNpcs>();
|
||||
self.start_rule::<rule::simulate_npcs::SimulateNpcs>();
|
||||
self.start_rule::<rule::npc_ai::NpcAi>();
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
pub mod migrate;
|
||||
pub mod npc_ai;
|
||||
pub mod replenish_resources;
|
||||
pub mod setup;
|
||||
pub mod simulate_npcs;
|
||||
pub mod sync_npcs;
|
||||
|
||||
use super::RtState;
|
||||
use std::fmt;
|
||||
|
@ -4,9 +4,9 @@ use tracing::warn;
|
||||
/// This rule runs at rtsim startup and broadly acts to perform some primitive
|
||||
/// migration/sanitisation in order to ensure that the state of rtsim is mostly
|
||||
/// sensible.
|
||||
pub struct Setup;
|
||||
pub struct Migrate;
|
||||
|
||||
impl Rule for Setup {
|
||||
impl Rule for Migrate {
|
||||
fn start(rtstate: &mut RtState) -> Result<Self, RuleError> {
|
||||
rtstate.bind::<Self, OnSetup>(|ctx| {
|
||||
let data = &mut *ctx.state.data_mut();
|
@ -20,7 +20,7 @@ use common::{
|
||||
vol::RectVolSize,
|
||||
};
|
||||
use fxhash::FxHasher64;
|
||||
use itertools::Itertools;
|
||||
use itertools::{Either, Itertools};
|
||||
use rand::prelude::*;
|
||||
use rand_chacha::ChaChaRng;
|
||||
use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator};
|
||||
@ -325,7 +325,7 @@ where
|
||||
}
|
||||
|
||||
if let Some(path) = path_site(wpos, site_exit, site, ctx.index) {
|
||||
Some(itertools::Either::Left(
|
||||
Some(Either::Left(
|
||||
seq(path.into_iter().map(|wpos| goto_2d(wpos, 1.0, 8.0))).then(goto_2d(
|
||||
site_exit,
|
||||
speed_factor,
|
||||
@ -333,14 +333,10 @@ where
|
||||
)),
|
||||
))
|
||||
} else {
|
||||
Some(itertools::Either::Right(goto_2d(
|
||||
site_exit,
|
||||
speed_factor,
|
||||
8.0,
|
||||
)))
|
||||
Some(Either::Right(goto_2d(site_exit, speed_factor, 8.0)))
|
||||
}
|
||||
} else {
|
||||
Some(itertools::Either::Right(goto_2d(wpos, speed_factor, 8.0)))
|
||||
Some(Either::Right(goto_2d(wpos, speed_factor, 8.0)))
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -410,9 +406,9 @@ fn travel_to_site(tgt_site: SiteId, speed_factor: f32) -> impl Action {
|
||||
// let track_len = ctx.world.civs().tracks.get(track_id).path().len();
|
||||
// // Tracks can be traversed backward (i.e: from end to beginning). Account for this.
|
||||
// seq(if reversed {
|
||||
// itertools::Either::Left((0..track_len).rev())
|
||||
// Either::Left((0..track_len).rev())
|
||||
// } else {
|
||||
// itertools::Either::Right(0..track_len)
|
||||
// Either::Right(0..track_len)
|
||||
// }
|
||||
// .enumerate()
|
||||
// .map(move |(i, node_idx)| now(move |ctx| {
|
||||
@ -456,7 +452,7 @@ fn timeout(time: f64) -> impl FnMut(&mut NpcCtx) -> bool + Clone + Send + Sync {
|
||||
fn socialize() -> impl Action {
|
||||
now(|ctx| {
|
||||
// TODO: Bit odd, should wait for a while after greeting
|
||||
if ctx.rng.gen_bool(0.0003) && let Some(other) = ctx
|
||||
if ctx.rng.gen_bool(0.004) && let Some(other) = ctx
|
||||
.state
|
||||
.data()
|
||||
.npcs
|
||||
@ -680,20 +676,18 @@ fn villager(visiting_site: SiteId) -> impl Action {
|
||||
})
|
||||
{
|
||||
// Walk to the plaza...
|
||||
travel_to_point(plaza_wpos, 0.5)
|
||||
.debug(|| "walk to plaza")
|
||||
// ...then wait for some time before moving on
|
||||
.then({
|
||||
let wait_time = ctx.rng.gen_range(30.0..90.0);
|
||||
socialize().repeat().stop_if(timeout(wait_time))
|
||||
.debug(|| "wait at plaza")
|
||||
})
|
||||
.map(|_| ())
|
||||
.boxed()
|
||||
Either::Left(travel_to_point(plaza_wpos, 0.5)
|
||||
.debug(|| "walk to plaza"))
|
||||
} else {
|
||||
// No plazas? :(
|
||||
finish().boxed()
|
||||
Either::Right(finish())
|
||||
}
|
||||
// ...then socialize for some time before moving on
|
||||
.then(socialize()
|
||||
.repeat()
|
||||
.stop_if(timeout(ctx.rng.gen_range(30.0..90.0)))
|
||||
.debug(|| "wait at plaza"))
|
||||
.map(|_| ())
|
||||
}))
|
||||
})
|
||||
.debug(move || format!("villager at site {:?}", visiting_site))
|
||||
|
@ -1,16 +1,16 @@
|
||||
use crate::{
|
||||
data::{npc::SimulationMode, Npc},
|
||||
event::{OnDeath, OnSetup, OnTick},
|
||||
event::{EventCtx, OnDeath, OnSetup, OnTick},
|
||||
RtState, Rule, RuleError,
|
||||
};
|
||||
use common::{
|
||||
comp::{self, Body},
|
||||
grid::Grid,
|
||||
rtsim::{Actor, NpcAction, NpcActivity, Personality},
|
||||
terrain::TerrainChunkSize,
|
||||
vol::RectVolSize,
|
||||
};
|
||||
use rand::{rngs::ThreadRng, seq::SliceRandom, Rng};
|
||||
use rand::prelude::*;
|
||||
use rand_chacha::ChaChaRng;
|
||||
use tracing::warn;
|
||||
use world::site::SiteKind;
|
||||
|
||||
@ -18,295 +18,243 @@ 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 = Actor::Npc(npc_id);
|
||||
vehicle.riders.push(actor);
|
||||
if ride.steering && vehicle.driver.replace(actor).is_some() {
|
||||
panic!("Replaced driver");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(home) = npc.home.and_then(|home| data.sites.get_mut(home)) {
|
||||
home.population.insert(npc_id);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
rtstate.bind::<Self, OnDeath>(|ctx| {
|
||||
let data = &mut *ctx.state.data_mut();
|
||||
let npc_id = ctx.event.npc_id;
|
||||
let Some(npc) = data.npcs.get(npc_id) else {
|
||||
return;
|
||||
};
|
||||
if let Some(home) = npc.home.and_then(|home| data.sites.get_mut(home)) {
|
||||
home.population.remove(&npc_id);
|
||||
}
|
||||
let mut rng = rand::thread_rng();
|
||||
match npc.body {
|
||||
Body::Humanoid(_) => {
|
||||
if let Some((site_id, site)) = data
|
||||
.sites
|
||||
.iter()
|
||||
.filter(|(id, site)| {
|
||||
Some(*id) != npc.home
|
||||
&& site.faction == npc.faction
|
||||
&& site.world_site.map_or(false, |s| {
|
||||
matches!(ctx.index.sites.get(s).kind, SiteKind::Refactor(_))
|
||||
})
|
||||
})
|
||||
.min_by_key(|(_, site)| site.population.len())
|
||||
{
|
||||
let rand_wpos = |rng: &mut ThreadRng| {
|
||||
let wpos2d = site.wpos.map(|e| e + rng.gen_range(-10..10));
|
||||
wpos2d
|
||||
.map(|e| e as f32 + 0.5)
|
||||
.with_z(ctx.world.sim().get_alt_approx(wpos2d).unwrap_or(0.0))
|
||||
};
|
||||
let random_humanoid = |rng: &mut ThreadRng| {
|
||||
let species = comp::humanoid::ALL_SPECIES.choose(&mut *rng).unwrap();
|
||||
Body::Humanoid(comp::humanoid::Body::random_with(rng, species))
|
||||
};
|
||||
data.spawn_npc(
|
||||
Npc::new(rng.gen(), rand_wpos(&mut rng), random_humanoid(&mut rng))
|
||||
.with_personality(Personality::random(&mut rng))
|
||||
.with_home(site_id)
|
||||
.with_faction(npc.faction)
|
||||
.with_profession(npc.profession.clone()),
|
||||
);
|
||||
} else {
|
||||
warn!("No site found for respawning humaniod");
|
||||
}
|
||||
},
|
||||
Body::BirdLarge(_) => {
|
||||
if let Some((site_id, site)) = data
|
||||
.sites
|
||||
.iter()
|
||||
.filter(|(id, site)| {
|
||||
Some(*id) != npc.home
|
||||
&& site.world_site.map_or(false, |s| {
|
||||
matches!(ctx.index.sites.get(s).kind, SiteKind::Dungeon(_))
|
||||
})
|
||||
})
|
||||
.min_by_key(|(_, site)| site.population.len())
|
||||
{
|
||||
let rand_wpos = |rng: &mut ThreadRng| {
|
||||
let wpos2d = site.wpos.map(|e| e + rng.gen_range(-10..10));
|
||||
wpos2d
|
||||
.map(|e| e as f32 + 0.5)
|
||||
.with_z(ctx.world.sim().get_alt_approx(wpos2d).unwrap_or(0.0))
|
||||
};
|
||||
let species = [
|
||||
comp::body::bird_large::Species::Phoenix,
|
||||
comp::body::bird_large::Species::Cockatrice,
|
||||
comp::body::bird_large::Species::Roc,
|
||||
]
|
||||
.choose(&mut rng)
|
||||
.unwrap();
|
||||
data.npcs.create_npc(
|
||||
Npc::new(
|
||||
rng.gen(),
|
||||
rand_wpos(&mut rng),
|
||||
Body::BirdLarge(comp::body::bird_large::Body::random_with(
|
||||
&mut rng, species,
|
||||
)),
|
||||
)
|
||||
.with_home(site_id),
|
||||
);
|
||||
} else {
|
||||
warn!("No site found for respawning bird");
|
||||
}
|
||||
},
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
});
|
||||
|
||||
rtstate.bind::<Self, OnTick>(|ctx| {
|
||||
let data = &mut *ctx.state.data_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
|
||||
.sim()
|
||||
.get(npc.wpos.xy().as_::<i32>() / TerrainChunkSize::RECT_SIZE.as_())
|
||||
.and_then(|chunk| {
|
||||
chunk
|
||||
.sites
|
||||
.iter()
|
||||
.find_map(|site| data.sites.world_site_map.get(site).copied())
|
||||
});
|
||||
|
||||
let chunk_pos =
|
||||
npc.wpos.xy().as_::<i32>() / TerrainChunkSize::RECT_SIZE.as_::<i32>();
|
||||
if npc.chunk_pos != Some(chunk_pos) {
|
||||
if let Some(cell) = npc
|
||||
.chunk_pos
|
||||
.and_then(|chunk_pos| data.npcs.npc_grid.get_mut(chunk_pos))
|
||||
{
|
||||
if let Some(index) = cell.npcs.iter().position(|id| *id == npc_id) {
|
||||
cell.npcs.swap_remove(index);
|
||||
}
|
||||
}
|
||||
npc.chunk_pos = Some(chunk_pos);
|
||||
if let Some(cell) = data.npcs.npc_grid.get_mut(chunk_pos) {
|
||||
cell.npcs.push(npc_id);
|
||||
}
|
||||
}
|
||||
|
||||
// Simulate the NPC's movement and interactions
|
||||
if matches!(npc.mode, SimulationMode::Simulated) {
|
||||
// Simulate NPC movement when riding
|
||||
if let Some(riding) = &npc.riding {
|
||||
if let Some(vehicle) = data.npcs.vehicles.get_mut(riding.vehicle) {
|
||||
match npc.controller.activity {
|
||||
// If steering, the NPC controls the vehicle's motion
|
||||
Some(NpcActivity::Goto(target, speed_factor))
|
||||
if riding.steering =>
|
||||
{
|
||||
let diff = target.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.body {
|
||||
common::comp::ship::Body::DefaultAirship
|
||||
| common::comp::ship::Body::AirBalloon => true,
|
||||
common::comp::ship::Body::SailBoat
|
||||
| common::comp::ship::Body::Galleon => {
|
||||
let chunk_pos = wpos.xy().as_::<i32>()
|
||||
/ TerrainChunkSize::RECT_SIZE.as_::<i32>();
|
||||
ctx.world
|
||||
.sim()
|
||||
.get(chunk_pos)
|
||||
.map_or(true, |f| f.river.river_kind.is_some())
|
||||
},
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if is_valid {
|
||||
match vehicle.body {
|
||||
common::comp::ship::Body::DefaultAirship
|
||||
| common::comp::ship::Body::AirBalloon => {
|
||||
if let Some(alt) = ctx
|
||||
.world
|
||||
.sim()
|
||||
.get_alt_approx(wpos.xy().as_())
|
||||
.filter(|alt| wpos.z < *alt)
|
||||
{
|
||||
wpos.z = alt;
|
||||
}
|
||||
},
|
||||
common::comp::ship::Body::SailBoat
|
||||
| common::comp::ship::Body::Galleon => {
|
||||
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;
|
||||
}
|
||||
}
|
||||
},
|
||||
// When riding, other actions are disabled
|
||||
Some(
|
||||
NpcActivity::Goto(_, _)
|
||||
| NpcActivity::Gather(_)
|
||||
| NpcActivity::HuntAnimals
|
||||
| NpcActivity::Dance,
|
||||
) => {},
|
||||
None => {},
|
||||
}
|
||||
npc.wpos = vehicle.wpos;
|
||||
} else {
|
||||
// Vehicle doens't exist anymore
|
||||
npc.riding = None;
|
||||
}
|
||||
// If not riding, we assume they're just walking
|
||||
} else {
|
||||
match npc.controller.activity {
|
||||
// Move NPCs if they have a target destination
|
||||
Some(NpcActivity::Goto(target, speed_factor)) => {
|
||||
let diff = target.xy() - npc.wpos.xy();
|
||||
let dist2 = diff.magnitude_squared();
|
||||
|
||||
if dist2 > 0.5f32.powi(2) {
|
||||
npc.wpos += (diff
|
||||
* (npc.body.max_speed_approx()
|
||||
* speed_factor
|
||||
* ctx.event.dt
|
||||
/ dist2.sqrt())
|
||||
.min(1.0))
|
||||
.with_z(0.0);
|
||||
}
|
||||
},
|
||||
Some(
|
||||
NpcActivity::Gather(_)
|
||||
| NpcActivity::HuntAnimals
|
||||
| NpcActivity::Dance,
|
||||
) => {
|
||||
// TODO: Maybe they should walk around randomly
|
||||
// when gathering resources?
|
||||
},
|
||||
None => {},
|
||||
}
|
||||
}
|
||||
|
||||
// Consume NPC actions
|
||||
for action in std::mem::take(&mut npc.controller.actions) {
|
||||
match action {
|
||||
NpcAction::Greet(_) | NpcAction::Say(_) => {}, // Currently, just swallow interactions
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure NPCs remain on the surface
|
||||
npc.wpos.z = ctx
|
||||
.world
|
||||
.sim()
|
||||
.get_surface_alt_approx(npc.wpos.xy().map(|e| e as i32))
|
||||
.unwrap_or(0.0)
|
||||
+ npc.body.flying_height();
|
||||
}
|
||||
}
|
||||
});
|
||||
rtstate.bind::<Self, OnSetup>(on_setup);
|
||||
rtstate.bind::<Self, OnDeath>(on_death);
|
||||
rtstate.bind::<Self, OnTick>(on_tick);
|
||||
|
||||
Ok(Self)
|
||||
}
|
||||
}
|
||||
|
||||
fn on_setup(ctx: EventCtx<SimulateNpcs, OnSetup>) {
|
||||
let data = &mut *ctx.state.data_mut();
|
||||
|
||||
// Add riders to vehicles
|
||||
for (npc_id, npc) in data.npcs.npcs.iter() {
|
||||
if let Some(ride) = &npc.riding {
|
||||
if let Some(vehicle) = data.npcs.vehicles.get_mut(ride.vehicle) {
|
||||
let actor = Actor::Npc(npc_id);
|
||||
vehicle.riders.push(actor);
|
||||
if ride.steering && vehicle.driver.replace(actor).is_some() {
|
||||
panic!("Replaced driver");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_death(ctx: EventCtx<SimulateNpcs, OnDeath>) {
|
||||
let data = &mut *ctx.state.data_mut();
|
||||
let npc_id = ctx.event.npc_id;
|
||||
let Some(npc) = data.npcs.get(npc_id) else {
|
||||
return;
|
||||
};
|
||||
let mut rng = ChaChaRng::from_seed(thread_rng().gen::<[u8; 32]>());
|
||||
|
||||
// Respawn dead NPCs
|
||||
match npc.body {
|
||||
Body::Humanoid(_) => {
|
||||
if let Some((site_id, site)) = data
|
||||
.sites
|
||||
.iter()
|
||||
.filter(|(id, site)| {
|
||||
Some(*id) != npc.home
|
||||
&& site.faction == npc.faction
|
||||
&& site.world_site.map_or(false, |s| {
|
||||
matches!(ctx.index.sites.get(s).kind, SiteKind::Refactor(_))
|
||||
})
|
||||
})
|
||||
.min_by_key(|(_, site)| site.population.len())
|
||||
{
|
||||
let rand_wpos = |rng: &mut ChaChaRng| {
|
||||
let wpos2d = site.wpos.map(|e| e + rng.gen_range(-10..10));
|
||||
wpos2d
|
||||
.map(|e| e as f32 + 0.5)
|
||||
.with_z(ctx.world.sim().get_alt_approx(wpos2d).unwrap_or(0.0))
|
||||
};
|
||||
let random_humanoid = |rng: &mut ChaChaRng| {
|
||||
let species = comp::humanoid::ALL_SPECIES.choose(&mut *rng).unwrap();
|
||||
Body::Humanoid(comp::humanoid::Body::random_with(rng, species))
|
||||
};
|
||||
data.spawn_npc(
|
||||
Npc::new(rng.gen(), rand_wpos(&mut rng), random_humanoid(&mut rng))
|
||||
.with_personality(Personality::random(&mut rng))
|
||||
.with_home(site_id)
|
||||
.with_faction(npc.faction)
|
||||
.with_profession(npc.profession.clone()),
|
||||
);
|
||||
} else {
|
||||
warn!("No site found for respawning humaniod");
|
||||
}
|
||||
},
|
||||
Body::BirdLarge(_) => {
|
||||
if let Some((site_id, site)) = data
|
||||
.sites
|
||||
.iter()
|
||||
.filter(|(id, site)| {
|
||||
Some(*id) != npc.home
|
||||
&& site.world_site.map_or(false, |s| {
|
||||
matches!(ctx.index.sites.get(s).kind, SiteKind::Dungeon(_))
|
||||
})
|
||||
})
|
||||
.min_by_key(|(_, site)| site.population.len())
|
||||
{
|
||||
let rand_wpos = |rng: &mut ChaChaRng| {
|
||||
let wpos2d = site.wpos.map(|e| e + rng.gen_range(-10..10));
|
||||
wpos2d
|
||||
.map(|e| e as f32 + 0.5)
|
||||
.with_z(ctx.world.sim().get_alt_approx(wpos2d).unwrap_or(0.0))
|
||||
};
|
||||
let species = [
|
||||
comp::body::bird_large::Species::Phoenix,
|
||||
comp::body::bird_large::Species::Cockatrice,
|
||||
comp::body::bird_large::Species::Roc,
|
||||
]
|
||||
.choose(&mut rng)
|
||||
.unwrap();
|
||||
data.npcs.create_npc(
|
||||
Npc::new(
|
||||
rng.gen(),
|
||||
rand_wpos(&mut rng),
|
||||
Body::BirdLarge(comp::body::bird_large::Body::random_with(
|
||||
&mut rng, species,
|
||||
)),
|
||||
)
|
||||
.with_home(site_id),
|
||||
);
|
||||
} else {
|
||||
warn!("No site found for respawning bird");
|
||||
}
|
||||
},
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_tick(ctx: EventCtx<SimulateNpcs, OnTick>) {
|
||||
let data = &mut *ctx.state.data_mut();
|
||||
for npc in data
|
||||
.npcs
|
||||
.npcs
|
||||
.values_mut()
|
||||
.filter(|npc| matches!(npc.mode, SimulationMode::Simulated))
|
||||
{
|
||||
// Simulate NPC movement when riding
|
||||
if let Some(riding) = &npc.riding {
|
||||
if let Some(vehicle) = data.npcs.vehicles.get_mut(riding.vehicle) {
|
||||
match npc.controller.activity {
|
||||
// If steering, the NPC controls the vehicle's motion
|
||||
Some(NpcActivity::Goto(target, speed_factor)) if riding.steering => {
|
||||
let diff = target.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.body {
|
||||
common::comp::ship::Body::DefaultAirship
|
||||
| common::comp::ship::Body::AirBalloon => true,
|
||||
common::comp::ship::Body::SailBoat
|
||||
| common::comp::ship::Body::Galleon => {
|
||||
let chunk_pos =
|
||||
wpos.xy().as_::<i32>().map2(
|
||||
TerrainChunkSize::RECT_SIZE.as_::<i32>(),
|
||||
|e, sz| e.div_euclid(sz),
|
||||
);
|
||||
ctx.world
|
||||
.sim()
|
||||
.get(chunk_pos)
|
||||
.map_or(true, |f| f.river.river_kind.is_some())
|
||||
},
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if is_valid {
|
||||
match vehicle.body {
|
||||
common::comp::ship::Body::DefaultAirship
|
||||
| common::comp::ship::Body::AirBalloon => {
|
||||
if let Some(alt) = ctx
|
||||
.world
|
||||
.sim()
|
||||
.get_alt_approx(wpos.xy().as_())
|
||||
.filter(|alt| wpos.z < *alt)
|
||||
{
|
||||
wpos.z = alt;
|
||||
}
|
||||
},
|
||||
common::comp::ship::Body::SailBoat
|
||||
| common::comp::ship::Body::Galleon => {
|
||||
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;
|
||||
}
|
||||
}
|
||||
},
|
||||
// When riding, other actions are disabled
|
||||
Some(
|
||||
NpcActivity::Goto(_, _)
|
||||
| NpcActivity::Gather(_)
|
||||
| NpcActivity::HuntAnimals
|
||||
| NpcActivity::Dance,
|
||||
) => {},
|
||||
None => {},
|
||||
}
|
||||
npc.wpos = vehicle.wpos;
|
||||
} else {
|
||||
// Vehicle doens't exist anymore
|
||||
npc.riding = None;
|
||||
}
|
||||
// If not riding, we assume they're just walking
|
||||
} else {
|
||||
match npc.controller.activity {
|
||||
// Move NPCs if they have a target destination
|
||||
Some(NpcActivity::Goto(target, speed_factor)) => {
|
||||
let diff = target.xy() - npc.wpos.xy();
|
||||
let dist2 = diff.magnitude_squared();
|
||||
|
||||
if dist2 > 0.5f32.powi(2) {
|
||||
npc.wpos += (diff
|
||||
* (npc.body.max_speed_approx() * speed_factor * ctx.event.dt
|
||||
/ dist2.sqrt())
|
||||
.min(1.0))
|
||||
.with_z(0.0);
|
||||
}
|
||||
},
|
||||
Some(NpcActivity::Gather(_) | NpcActivity::HuntAnimals | NpcActivity::Dance) => {
|
||||
// TODO: Maybe they should walk around randomly
|
||||
// when gathering resources?
|
||||
},
|
||||
None => {},
|
||||
}
|
||||
}
|
||||
|
||||
// Consume NPC actions
|
||||
for action in std::mem::take(&mut npc.controller.actions) {
|
||||
match action {
|
||||
NpcAction::Greet(_) | NpcAction::Say(_) => {}, /* Currently, just swallow
|
||||
* interactions */
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure NPCs remain on the surface
|
||||
npc.wpos.z = ctx
|
||||
.world
|
||||
.sim()
|
||||
.get_surface_alt_approx(npc.wpos.xy().map(|e| e as i32))
|
||||
.unwrap_or(0.0)
|
||||
+ npc.body.flying_height();
|
||||
}
|
||||
}
|
||||
|
112
rtsim/src/rule/sync_npcs.rs
Normal file
112
rtsim/src/rule/sync_npcs.rs
Normal file
@ -0,0 +1,112 @@
|
||||
use crate::{
|
||||
event::{EventCtx, OnDeath, OnSetup, OnTick},
|
||||
RtState, Rule, RuleError,
|
||||
};
|
||||
use common::{grid::Grid, terrain::TerrainChunkSize, vol::RectVolSize};
|
||||
|
||||
pub struct SyncNpcs;
|
||||
|
||||
impl Rule for SyncNpcs {
|
||||
fn start(rtstate: &mut RtState) -> Result<Self, RuleError> {
|
||||
rtstate.bind::<Self, OnSetup>(on_setup);
|
||||
rtstate.bind::<Self, OnDeath>(on_death);
|
||||
rtstate.bind::<Self, OnTick>(on_tick);
|
||||
|
||||
Ok(Self)
|
||||
}
|
||||
}
|
||||
|
||||
fn on_setup(ctx: EventCtx<SyncNpcs, OnSetup>) {
|
||||
let data = &mut *ctx.state.data_mut();
|
||||
|
||||
// Create NPC grid
|
||||
data.npcs.npc_grid = Grid::new(ctx.world.sim().get_size().as_(), Default::default());
|
||||
|
||||
// Add NPCs to home population (TODO: Do this on entity creation?)
|
||||
for (npc_id, npc) in data.npcs.npcs.iter() {
|
||||
if let Some(home) = npc.home.and_then(|home| data.sites.get_mut(home)) {
|
||||
home.population.insert(npc_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_death(ctx: EventCtx<SyncNpcs, OnDeath>) {
|
||||
let data = &mut *ctx.state.data_mut();
|
||||
|
||||
println!("NPC DIED!");
|
||||
|
||||
// Remove NPC from home population
|
||||
if let Some(home) = data
|
||||
.npcs
|
||||
.get(ctx.event.npc_id)
|
||||
.and_then(|npc| npc.home)
|
||||
.and_then(|home| data.sites.get_mut(home))
|
||||
{
|
||||
home.population.remove(&ctx.event.npc_id);
|
||||
}
|
||||
}
|
||||
|
||||
fn on_tick(ctx: EventCtx<SyncNpcs, OnTick>) {
|
||||
let data = &mut *ctx.state.data_mut();
|
||||
// Update vehicle grid cells
|
||||
for (vehicle_id, vehicle) in data.npcs.vehicles.iter_mut() {
|
||||
let chunk_pos = vehicle.wpos.xy().as_::<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
|
||||
.sim()
|
||||
.get(
|
||||
npc.wpos
|
||||
.xy()
|
||||
.as_::<i32>()
|
||||
.map2(TerrainChunkSize::RECT_SIZE.as_::<i32>(), |e, sz| {
|
||||
e.div_euclid(sz)
|
||||
}),
|
||||
)
|
||||
.and_then(|chunk| {
|
||||
chunk
|
||||
.sites
|
||||
.iter()
|
||||
.find_map(|site| data.sites.world_site_map.get(site).copied())
|
||||
});
|
||||
|
||||
// Update the NPC's grid cell
|
||||
let chunk_pos = npc
|
||||
.wpos
|
||||
.xy()
|
||||
.as_::<i32>()
|
||||
.map2(TerrainChunkSize::RECT_SIZE.as_::<i32>(), |e, sz| {
|
||||
e.div_euclid(sz)
|
||||
});
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -224,30 +224,23 @@ impl<'a> System<'a> for Sys {
|
||||
data.time_of_day = *time_of_day;
|
||||
|
||||
// Update character map (i.e: so that rtsim knows where players are)
|
||||
// TODO: Other entities too? Or do we now care about that?
|
||||
// TODO: Other entities too like animals? Or do we now care about that?
|
||||
data.npcs.character_map.clear();
|
||||
for (character, wpos) in
|
||||
(&presences, &positions)
|
||||
.join()
|
||||
.filter_map(|(presence, pos)| {
|
||||
if let PresenceKind::Character(character) = &presence.kind {
|
||||
Some((character, pos.0))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
{
|
||||
let chunk_pos = wpos
|
||||
.xy()
|
||||
.as_::<i32>()
|
||||
.map2(TerrainChunkSize::RECT_SIZE.as_::<i32>(), |e, sz| {
|
||||
e.div_euclid(sz)
|
||||
});
|
||||
data.npcs
|
||||
.character_map
|
||||
.entry(chunk_pos)
|
||||
.or_default()
|
||||
.push((*character, wpos));
|
||||
for (presence, wpos) in (&presences, &positions).join() {
|
||||
if let PresenceKind::Character(character) = &presence.kind {
|
||||
let chunk_pos = wpos
|
||||
.0
|
||||
.xy()
|
||||
.as_::<i32>()
|
||||
.map2(TerrainChunkSize::RECT_SIZE.as_::<i32>(), |e, sz| {
|
||||
e.div_euclid(sz)
|
||||
});
|
||||
data.npcs
|
||||
.character_map
|
||||
.entry(chunk_pos)
|
||||
.or_default()
|
||||
.push((*character, wpos.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -477,18 +477,18 @@ fn handle_rtsim_actions(bdata: &mut BehaviorData) -> bool {
|
||||
NpcAction::Greet(actor) => {
|
||||
if bdata.agent.allowed_to_speak()
|
||||
&& let Some(target) = bdata.read_data.lookup_actor(actor)
|
||||
&& let Some(target_pos) = bdata.read_data.positions.get(target)
|
||||
{
|
||||
bdata.agent.target = Some(Target::new(
|
||||
target,
|
||||
false,
|
||||
bdata.read_data.time.0,
|
||||
false,
|
||||
Some(target_pos.0),
|
||||
bdata.read_data.positions.get(target).map(|p| p.0),
|
||||
));
|
||||
// We're always aware of someone we're talking to
|
||||
bdata.agent.awareness.set_maximally_aware();
|
||||
|
||||
bdata.controller.push_action(ControlAction::Stand);
|
||||
bdata.controller.push_action(ControlAction::Talk);
|
||||
bdata.controller.push_utterance(UtteranceKind::Greeting);
|
||||
bdata
|
||||
@ -499,20 +499,15 @@ fn handle_rtsim_actions(bdata: &mut BehaviorData) -> bool {
|
||||
.agent
|
||||
.timer
|
||||
.start(bdata.read_data.time.0, TimerAction::Interact);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
},
|
||||
NpcAction::Say(msg) => {
|
||||
bdata.controller.push_utterance(UtteranceKind::Greeting);
|
||||
bdata.agent_data.chat_npc(msg, bdata.event_emitter);
|
||||
false
|
||||
},
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Handle timed events, like looking at the player we are talking to
|
||||
|
Loading…
Reference in New Issue
Block a user