diff --git a/Cargo.lock b/Cargo.lock index 237da3cbc5..292eded160 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6948,6 +6948,7 @@ dependencies = [ "anymap2", "atomic_refcell", "enum-map", + "fxhash", "hashbrown 0.12.3", "rand 0.8.5", "rmp-serde", diff --git a/rtsim/Cargo.toml b/rtsim/Cargo.toml index da380b2066..eb01cb8640 100644 --- a/rtsim/Cargo.toml +++ b/rtsim/Cargo.toml @@ -17,3 +17,4 @@ tracing = "0.1" atomic_refcell = "0.1" slotmap = { version = "1.0.6", features = ["serde"] } rand = { version = "0.8", features = ["small_rng"] } +fxhash = "0.2.1" \ No newline at end of file diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index 8579bba26e..cdd866beb0 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -1,11 +1,131 @@ -use crate::{data::npc::NpcMode, event::OnTick, RtState, Rule, RuleError}; -use rand::seq::IteratorRandom; -use tracing::info; +use std::hash::BuildHasherDefault; + +use crate::{ + event::OnTick, + RtState, Rule, RuleError, +}; +use common::{astar::{Astar, PathResult}, store::Id}; +use fxhash::FxHasher64; +use rand::{seq::IteratorRandom, rngs::SmallRng, SeedableRng}; use vek::*; -use world::site::SiteKind; +use world::{ + site::{Site as WorldSite, SiteKind}, + site2::{self, TileKind}, + IndexRef, +}; pub struct NpcAi; +const NEIGHBOURS: &[Vec2] = &[ + Vec2::new(1, 0), + Vec2::new(0, 1), + Vec2::new(-1, 0), + Vec2::new(0, -1), + Vec2::new(1, 1), + Vec2::new(-1, 1), + Vec2::new(-1, -1), + Vec2::new(1, -1), +]; +const CARDINALS: &[Vec2] = &[ + Vec2::new(1, 0), + Vec2::new(0, 1), + Vec2::new(-1, 0), + Vec2::new(0, -1), +]; + +fn path_between(start: Vec2, end: Vec2, site: &site2::Site) -> PathResult> { + let heuristic = |tile: &Vec2| tile.as_::().distance(end.as_()); + let mut astar = Astar::new( + 100, + start, + &heuristic, + BuildHasherDefault::::default(), + ); + + let transition = |a: &Vec2, b: &Vec2| { + let distance = a.as_::().distance(b.as_()); + let a_tile = site.tiles.get(*a); + let b_tile = site.tiles.get(*b); + + let terrain = match &b_tile.kind { + TileKind::Empty => 5.0, + TileKind::Hazard(_) => 20.0, + TileKind::Field => 12.0, + TileKind::Plaza + | TileKind::Road { .. } => 1.0, + + TileKind::Building + | TileKind::Castle + | TileKind::Wall(_) + | TileKind::Tower(_) + | TileKind::Keep(_) + | TileKind::Gate + | TileKind::GnarlingFortification => 3.0, + }; + let is_door_tile = |plot: Id, tile: Vec2| { + match site.plot(plot).kind() { + site2::PlotKind::House(house) => house.door_tile == tile, + site2::PlotKind::Workshop(_) => true, + _ => false, + } + }; + let building = if a_tile.is_building() && b_tile.is_road() { + a_tile.plot.and_then(|plot| is_door_tile(plot, *a).then(|| 1.0)).unwrap_or(f32::INFINITY) + } else if b_tile.is_building() && a_tile.is_road() { + b_tile.plot.and_then(|plot| is_door_tile(plot, *b).then(|| 1.0)).unwrap_or(f32::INFINITY) + } else if (a_tile.is_building() || b_tile.is_building()) && a_tile.plot != b_tile.plot { + f32::INFINITY + } else { + 1.0 + }; + + distance * terrain * building + }; + + astar.poll( + 100, + heuristic, + |&tile| NEIGHBOURS.iter().map(move |c| tile + *c), + transition, + |tile| *tile == end, + ) +} + +fn path_town( + wpos: Vec3, + site: Id, + index: IndexRef, + time: f64, + seed: u32, +) -> Option<(Vec3, f32)> { + match &index.sites.get(site).kind { + SiteKind::Refactor(site) | SiteKind::CliffTown(site) | SiteKind::DesertCity(site) => { + let start = site.wpos_tile_pos(wpos.xy().as_()); + + let mut rng = SmallRng::from_seed([(time / 3.0) as u8 ^ seed as u8; 32]); + + let end = site.plots[site.plazas().choose(&mut rng)?].root_tile(); + + if start == end { + return None; + } + + let next_tile = match path_between(start, end, site) { + PathResult::None(p) | PathResult::Exhausted(p) | PathResult::Path(p) => p.into_iter().nth(2), + PathResult::Pending => None, + }.unwrap_or(end); + + let wpos = site.tile_center_wpos(next_tile).as_().with_z(wpos.z); + + Some((wpos, 1.0)) + }, + _ => { + // No brain T_T + None + }, + } +} + impl Rule for NpcAi { fn start(rtstate: &mut RtState) -> Result { rtstate.bind::(|ctx| { @@ -16,37 +136,11 @@ impl Rule for NpcAi { .and_then(|site_id| data.sites.get(site_id)?.world_site) { if let Some((target, _)) = npc.target { - if target.distance_squared(npc.wpos) < 1.0 { + if target.xy().distance_squared(npc.wpos.xy()) < 1.0 { npc.target = None; } } else { - match &ctx.index.sites.get(home_id).kind { - SiteKind::Refactor(site) - | SiteKind::CliffTown(site) - | SiteKind::DesertCity(site) => { - let tile = site.wpos_tile_pos(npc.wpos.xy().as_()); - - let mut rng = rand::thread_rng(); - let cardinals = [ - Vec2::unit_x(), - Vec2::unit_y(), - -Vec2::unit_x(), - -Vec2::unit_y(), - ]; - let next_tile = cardinals - .iter() - .map(|c| tile + *c) - .filter(|tile| site.tiles.get(*tile).is_road()).choose(&mut rng).unwrap_or(tile); - - let wpos = - site.tile_center_wpos(next_tile).as_().with_z(npc.wpos.z); - - npc.target = Some((wpos, 1.0)); - }, - _ => { - // No brain T_T - }, - } + npc.target = path_town(npc.wpos, home_id, ctx.index, ctx.event.time, npc.seed); } } else { // TODO: Don't make homeless people walk around in circles diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index 4d3bdd2b94..626499341f 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -292,6 +292,7 @@ impl StateExt for State { .with(comp::Combo::default()) .with(comp::Auras::default()) .with(comp::Stance::default()) + .with(RepositionOnChunkLoad) } fn create_object(&mut self, pos: comp::Pos, object: comp::object::Body) -> EcsEntityBuilder { diff --git a/world/src/site2/mod.rs b/world/src/site2/mod.rs index 95beeb6a87..8b46849d7d 100644 --- a/world/src/site2/mod.rs +++ b/world/src/site2/mod.rs @@ -3,11 +3,11 @@ pub mod plot; mod tile; pub mod util; -use self::tile::{HazardKind, KeepKind, RoofKind, Tile, TileGrid, TileKind, TILE_SIZE}; +use self::tile::{HazardKind, KeepKind, RoofKind, Tile, TileGrid, TILE_SIZE}; pub use self::{ gen::{aabr_with_z, Fill, Painter, Primitive, PrimitiveRef, Structure}, plot::{Plot, PlotKind}, - util::Dir, + util::Dir, tile::TileKind, }; use crate::{ sim::Path, @@ -40,7 +40,7 @@ fn reseed(rng: &mut impl Rng) -> impl Rng { ChaChaRng::from_seed(rng.gen::<[u8; #[derive(Default)] pub struct Site { - pub(crate) origin: Vec2, + pub origin: Vec2, name: String, // NOTE: Do we want these to be public? pub tiles: TileGrid, @@ -110,8 +110,8 @@ impl Site { pub fn bounds(&self) -> Aabr { let border = 1; Aabr { - min: self.origin + self.tile_wpos(self.tiles.bounds.min - border), - max: self.origin + self.tile_wpos(self.tiles.bounds.max + 1 + border), + min: self.tile_wpos(self.tiles.bounds.min - border), + max: self.tile_wpos(self.tiles.bounds.max + 1 + border), } } diff --git a/world/src/site2/plot/house.rs b/world/src/site2/plot/house.rs index 2a54ae1363..380c3af782 100644 --- a/world/src/site2/plot/house.rs +++ b/world/src/site2/plot/house.rs @@ -11,7 +11,7 @@ use vek::*; /// Represents house data generated by the `generate()` method pub struct House { /// Tile position of the door tile - door_tile: Vec2, + pub door_tile: Vec2, /// Axis aligned bounding region of tiles tile_aabr: Aabr, /// Axis aligned bounding region for the house diff --git a/world/src/site2/tile.rs b/world/src/site2/tile.rs index 5a3b951baa..a02849f4fe 100644 --- a/world/src/site2/tile.rs +++ b/world/src/site2/tile.rs @@ -193,8 +193,8 @@ pub enum TileKind { #[derive(Clone, PartialEq)] pub struct Tile { - pub(crate) kind: TileKind, - pub(crate) plot: Option>, + pub kind: TileKind, + pub plot: Option>, pub(crate) hard_alt: Option, }