Cleaned up rtsim rules

This commit is contained in:
Joshua Barretto 2023-04-05 13:57:41 +01:00
parent 3e0f5295c0
commit 5614eaa7a5
9 changed files with 397 additions and 353 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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