diff --git a/rtsim/Cargo.toml b/rtsim/Cargo.toml index 1d7665bcc4..321558b7b3 100644 --- a/rtsim/Cargo.toml +++ b/rtsim/Cargo.toml @@ -18,4 +18,4 @@ atomic_refcell = "0.1" slotmap = { version = "1.0.6", features = ["serde"] } rand = { version = "0.8", features = ["small_rng"] } fxhash = "0.2.1" -itertools = "0.10.3" \ No newline at end of file +itertools = "0.10.3" diff --git a/rtsim/src/data/npc.rs b/rtsim/src/data/npc.rs index bd97d8d097..a5d6f3dc93 100644 --- a/rtsim/src/data/npc.rs +++ b/rtsim/src/data/npc.rs @@ -11,7 +11,8 @@ use serde::{Deserialize, Serialize}; use slotmap::HopSlotMap; use std::{ collections::VecDeque, - ops::{Deref, DerefMut}, + ops::{Deref, DerefMut, ControlFlow}, + any::Any, }; use vek::*; use world::{civ::Track, site::Site as WorldSite, util::RandomPerm}; @@ -38,7 +39,113 @@ pub struct PathingMemory { pub intersite_path: Option<(PathData<(Id, bool), SiteId>, usize)>, } -#[derive(Clone, Serialize, Deserialize)] +pub struct Controller { + pub goto: Option<(Vec3, f32)>, +} + +#[derive(Default)] +pub struct TaskState { + state: Option>, +} + +pub const CONTINUE: ControlFlow<()> = ControlFlow::Break(()); +pub const FINISH: ControlFlow<()> = ControlFlow::Continue(()); + +pub trait Task: PartialEq + Clone + Send + Sync + 'static { + type State: Send + Sync; + type Ctx<'a>; + + fn begin<'a>(&self, ctx: &Self::Ctx<'a>) -> Self::State; + + fn run<'a>(&self, state: &mut Self::State, ctx: &Self::Ctx<'a>, controller: &mut Controller) -> ControlFlow<()>; + + fn then(self, other: B) -> Then { + Then(self, other) + } + + fn repeat(self) -> Repeat { + Repeat(self) + } +} + +#[derive(Clone, PartialEq)] +pub struct Then(A, B); + +impl Task for Then + where B: for<'a> Task = A::Ctx<'a>> +{ + type State = Result; // TODO: Use `Either` instead + type Ctx<'a> = A::Ctx<'a>; + + fn begin<'a>(&self, ctx: &Self::Ctx<'a>) -> Self::State { + Ok(self.0.begin(ctx)) + } + + fn run<'a>(&self, state: &mut Self::State, ctx: &Self::Ctx<'a>, controller: &mut Controller) -> ControlFlow<()> { + match state { + Ok(a_state) => { + self.0.run(a_state, ctx, controller)?; + *state = Err(self.1.begin(ctx)); + CONTINUE + }, + Err(b_state) => self.1.run(b_state, ctx, controller), + } + } +} + +#[derive(Clone, PartialEq)] +pub struct Repeat(A); + +impl Task for Repeat { + type State = A::State; + type Ctx<'a> = A::Ctx<'a>; + + fn begin<'a>(&self, ctx: &Self::Ctx<'a>) -> Self::State { + self.0.begin(ctx) + } + + fn run<'a>(&self, state: &mut Self::State, ctx: &Self::Ctx<'a>, controller: &mut Controller) -> ControlFlow<()> { + self.0.run(state, ctx, controller)?; + *state = self.0.begin(ctx); + CONTINUE + } +} + +impl TaskState { + pub fn perform<'a, T: Task>( + &mut self, + task: T, + ctx: &T::Ctx<'a>, + controller: &mut Controller, + ) -> ControlFlow<()> { + type StateOf = (T, ::State); + + let mut state = if let Some(state) = self.state + .take() + .and_then(|state| state + .downcast::>() + .ok() + .filter(|state| state.0 == task)) + { + state + } else { + let mut state = task.begin(ctx); + Box::new((task, state)) + }; + + let res = state.0.run(&mut state.1, ctx, controller); + + self.state = if matches!(res, ControlFlow::Break(())) { + Some(state) + } else { + None + }; + + res + } +} + +#[derive(Serialize, Deserialize)] pub struct Npc { // Persisted state /// Represents the location of the NPC. @@ -50,8 +157,6 @@ pub struct Npc { pub faction: Option, // Unpersisted state - #[serde(skip_serializing, skip_deserializing)] - pub pathing: PathingMemory, #[serde(skip_serializing, skip_deserializing)] pub current_site: Option, @@ -66,6 +171,26 @@ pub struct Npc { /// should instead be derived from the game. #[serde(skip_serializing, skip_deserializing)] pub mode: NpcMode, + + #[serde(skip_serializing, skip_deserializing)] + pub task_state: Option, +} + +impl Clone for Npc { + fn clone(&self) -> Self { + Self { + seed: self.seed, + wpos: self.wpos, + profession: self.profession.clone(), + home: self.home, + faction: self.faction, + // Not persisted + current_site: Default::default(), + goto: Default::default(), + mode: Default::default(), + task_state: Default::default(), + } + } } impl Npc { @@ -79,10 +204,10 @@ impl Npc { profession: None, home: None, faction: None, - pathing: Default::default(), current_site: None, goto: None, mode: NpcMode::Simulated, + task_state: Default::default(), } } diff --git a/rtsim/src/gen/mod.rs b/rtsim/src/gen/mod.rs index ee0ffe8b49..e2bcd7565b 100644 --- a/rtsim/src/gen/mod.rs +++ b/rtsim/src/gen/mod.rs @@ -77,13 +77,13 @@ impl Data { Npc::new(rng.gen(), rand_wpos(&mut rng)) .with_faction(site.faction) .with_home(site_id) - .with_profession(match rng.gen_range(0..20) { - 0 => Profession::Hunter, - 1 => Profession::Blacksmith, - 2 => Profession::Chef, - 3 => Profession::Alchemist, - 5..=10 => Profession::Farmer, - 11..=15 => Profession::Guard, + .with_profession(match rng.gen_range(0..17) { + // 0 => Profession::Hunter, + // 1 => Profession::Blacksmith, + // 2 => Profession::Chef, + // 3 => Profession::Alchemist, + // 5..=10 => Profession::Farmer, + // 11..=15 => Profession::Guard, _ => Profession::Adventurer(rng.gen_range(0..=3)), }), ); diff --git a/rtsim/src/lib.rs b/rtsim/src/lib.rs index 0a15a841bc..51cecf6575 100644 --- a/rtsim/src/lib.rs +++ b/rtsim/src/lib.rs @@ -1,4 +1,4 @@ -#![feature(explicit_generic_args_with_impl_trait)] +#![feature(explicit_generic_args_with_impl_trait, generic_associated_types, never_type, try_blocks)] pub mod data; pub mod event; diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index 8f5a02fbf9..103a528873 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -1,9 +1,9 @@ use std::{collections::VecDeque, hash::BuildHasherDefault}; use crate::{ - data::{npc::PathData, Sites}, + data::{npc::{PathData, PathingMemory, Npc, Task, TaskState, Controller, CONTINUE, FINISH}, Sites}, event::OnTick, - RtState, Rule, RuleError, + RtState, Rule, RuleError, EventCtx, }; use common::{ astar::{Astar, PathResult}, @@ -15,7 +15,7 @@ use common::{ }; use fxhash::FxHasher64; use itertools::Itertools; -use rand::seq::IteratorRandom; +use rand::prelude::*; use vek::*; use world::{ civ::{self, Track}, @@ -23,6 +23,11 @@ use world::{ site2::{self, TileKind}, IndexRef, World, }; +use std::{ + ops::ControlFlow, + marker::PhantomData, + any::{Any, TypeId}, +}; pub struct NpcAi; @@ -46,7 +51,7 @@ const CARDINALS: &[Vec2] = &[ fn path_in_site(start: Vec2, end: Vec2, site: &site2::Site) -> PathResult> { let heuristic = |tile: &Vec2| tile.as_::().distance(end.as_()); let mut astar = Astar::new( - 100, + 250, start, &heuristic, BuildHasherDefault::::default(), @@ -69,7 +74,7 @@ fn path_in_site(start: Vec2, end: Vec2, site: &site2::Site) -> PathRes | TileKind::Tower(_) | TileKind::Keep(_) | TileKind::Gate - | TileKind::GnarlingFortification => 3.0, + | TileKind::GnarlingFortification => 20.0, }; let is_door_tile = |plot: Id, tile: Vec2| match site.plot(plot).kind() { site2::PlotKind::House(house) => house.door_tile == tile, @@ -96,7 +101,7 @@ fn path_in_site(start: Vec2, end: Vec2, site: &site2::Site) -> PathRes }; astar.poll( - 100, + 250, heuristic, |&tile| NEIGHBOURS.iter().map(move |c| tile + *c), transition, @@ -132,7 +137,7 @@ fn path_between_sites( let heuristic = |site: &Id| get_site(site).center.as_().distance(end_pos); let mut astar = Astar::new( - 100, + 250, start, heuristic, BuildHasherDefault::::default(), @@ -149,7 +154,7 @@ fn path_between_sites( let transition = |a: &Id, b: &Id| track_between(*a, *b).cost; - let path = astar.poll(100, heuristic, neighbors, transition, |site| *site == end); + let path = astar.poll(250, heuristic, neighbors, transition, |site| *site == end); path.map(|path| { let path = path @@ -231,216 +236,314 @@ fn path_towns( } } +const MAX_STEP: f32 = 32.0; + impl Rule for NpcAi { fn start(rtstate: &mut RtState) -> Result { - rtstate.bind::(|ctx| { - let data = &mut *ctx.state.data_mut(); - let mut dynamic_rng = rand::thread_rng(); - for npc in data.npcs.values_mut() { - npc.current_site = ctx - .world - .sim() - .get(npc.wpos.xy().as_::() / TerrainChunkSize::RECT_SIZE.as_()) - .and_then(|chunk| data.sites.world_site_map.get(chunk.sites.first()?).copied()); + rtstate.bind::(|mut ctx| { + let npc_ids = ctx.state.data().npcs.keys().collect::>(); - if let Some(home_id) = npc.home { - if let Some((target, _)) = npc.goto { - // Walk to the current target - if target.xy().distance_squared(npc.wpos.xy()) < 4.0 { - npc.goto = None; - } - } else { - // Walk slower when pathing in a site, and faster when between sites - if npc.pathing.intersite_path.is_none() { - npc.goto = Some((npc.goto.map_or(npc.wpos, |(wpos, _)| wpos), 0.7)); - } else { - npc.goto = Some((npc.goto.map_or(npc.wpos, |(wpos, _)| wpos), 1.0)); - } + for npc_id in npc_ids { + let mut task_state = ctx.state.data_mut().npcs[npc_id].task_state.take().unwrap_or_default(); - if let Some((ref mut path, site)) = npc.pathing.intrasite_path { - // If the npc walking in a site and want to reroll (because the path was - // exhausted.) to try to find a complete path. - if path.repoll { - npc.pathing.intrasite_path = - path_town(npc.wpos, site, ctx.index, |_| Some(path.end)) - .map(|path| (path, site)); - } - } + let (controller, task_state) = { + let data = &*ctx.state.data(); + let npc = &data.npcs[npc_id]; - if let Some((ref mut path, site)) = npc.pathing.intrasite_path { - if let Some(next_tile) = path.path.pop_front() { - match &ctx.index.sites.get(site).kind { - SiteKind::Refactor(site) - | SiteKind::CliffTown(site) - | SiteKind::DesertCity(site) => { - // Set the target to the next node in the path. - let wpos = site.tile_center_wpos(next_tile); - let wpos = wpos.as_::().with_z( - ctx.world.sim().get_alt_approx(wpos).unwrap_or(0.0), - ); + let mut controller = Controller { goto: npc.goto }; - npc.goto = Some((wpos, npc.goto.map_or(1.0, |(_, sf)| sf))); - }, - _ => {}, - } - } else { - // If the path is empty, we're done. - npc.pathing.intrasite_path = None; - } - } else if let Some((path, progress)) = { - // Check if we are done with this part of the inter site path. - if let Some((path, progress)) = &mut npc.pathing.intersite_path { - if let Some((track_id, _)) = path.path.front() { - let track = ctx.world.civs().tracks.get(*track_id); - if *progress >= track.path().len() { - if path.repoll { - // Repoll if last path wasn't complete. - npc.pathing.intersite_path = path_towns( - npc.current_site.unwrap(), - path.end, - &data.sites, - ctx.world, - ); - } else { - // Otherwise just take the next in the calculated path. - path.path.pop_front(); - *progress = 0; - } - } - } - } - &mut npc.pathing.intersite_path - } { - if let Some((track_id, reversed)) = path.path.front() { - let track = ctx.world.civs().tracks.get(*track_id); - let get_progress = |progress: usize| { - if *reversed { - track.path().len().wrapping_sub(progress + 1) - } else { - progress - } - }; + let action: ControlFlow<()> = try { + if matches!(npc.profession, Some(Profession::Adventurer(_))) { + if let Some(home) = npc.home { + // Travel between random nearby sites + let task = generate(move |(npc, ctx): &(&Npc, &EventCtx<_, _>)| { + let tgt_site = ctx.state.data().sites + .iter() + .filter(|(site_id, site)| npc + .current_site + .map_or(true, |cs| *site_id != cs) && thread_rng().gen_bool(0.25)) + .min_by_key(|(_, site)| site.wpos.as_().distance(npc.wpos.xy()) as i32) + .map(|(site_id, _)| site_id) + .unwrap_or(home); - let transform_path_pos = |chunk_pos| { - let chunk_wpos = TerrainChunkSize::center_wpos(chunk_pos); - if let Some(pathdata) = - ctx.world.sim().get_nearest_path(chunk_wpos) - { - pathdata.1.map(|e| e as i32) - } else { - chunk_wpos - } - }; + TravelToSite(tgt_site) + }) + .repeat(); - // Loop through and skip nodes that are inside a site, and use intra - // site path finding there instead. - let walk_path = loop { - if let Some(chunk_pos) = - track.path().nodes.get(get_progress(*progress)) - { - if let Some((wpos, site_id, site)) = - ctx.world.sim().get(*chunk_pos).and_then(|chunk| { - let site_id = *chunk.sites.first()?; - let wpos = transform_path_pos(*chunk_pos); - match &ctx.index.sites.get(site_id).kind { - SiteKind::Refactor(site) - | SiteKind::CliffTown(site) - | SiteKind::DesertCity(site) - if !site.wpos_tile(wpos).is_empty() => - { - Some((wpos, site_id, site)) - }, - _ => None, - } - }) - { - if !site.wpos_tile(wpos).is_empty() { - *progress += 1; - } else { - let end = site.wpos_tile_pos(wpos); - npc.pathing.intrasite_path = - path_town(npc.wpos, site_id, ctx.index, |_| { - Some(end) - }) - .map(|path| (path, site_id)); - break false; - } - } else { - break true; - } - } else { - break false; - } - }; - - if walk_path { - // Find the next wpos on the path. - // NOTE: Consider not having this big gap between current - // position and next. For better path finding. Maybe that would - // mean having a float for progress. - let wpos = transform_path_pos( - track.path().nodes[get_progress(*progress)], - ); - let wpos = wpos.as_::().with_z( - ctx.world.sim().get_alt_approx(wpos).unwrap_or(0.0), - ); - npc.goto = Some((wpos, npc.goto.map_or(1.0, |(_, sf)| sf))); - *progress += 1; - } - } else { - npc.pathing.intersite_path = None; + task_state.perform(task, &(&*npc, &ctx), &mut controller)?; } } else { - if matches!(npc.profession, Some(Profession::Adventurer(_))) { - // If the npc is home, choose a random site to go to, otherwise go - // home. - if let Some(start) = npc.current_site { - let end = if home_id == start { - data.sites - .keys() - .filter(|site| *site != home_id) - .choose(&mut dynamic_rng) - .unwrap_or(home_id) - } else { - home_id - }; - npc.pathing.intersite_path = - path_towns(start, end, &data.sites, ctx.world); - } - } else { - // Choose a random plaza in the npcs home site (which should be the - // current here) to go to. - if let Some(home_id) = - data.sites.get(home_id).and_then(|site| site.world_site) - { - npc.pathing.intrasite_path = - path_town(npc.wpos, home_id, ctx.index, |site| { - Some( - site.plots - [site.plazas().choose(&mut dynamic_rng)?] - .root_tile(), - ) - }) - .map(|path| (path, home_id)); - } - } + controller.goto = None; + + // // Choose a random plaza in the npcs home site (which should be the + // // current here) to go to. + // if let Some(home_id) = + // data.sites.get(home_id).and_then(|site| site.world_site) + // { + // npc.pathing.intrasite_path = + // path_town(npc.wpos, home_id, ctx.index, |site| { + // Some( + // site.plots + // [site.plazas().choose(&mut dynamic_rng)?] + // .root_tile(), + // ) + // }) + // .map(|path| (path, home_id)); + // } } - } - } else { - // TODO: Don't make homeless people walk around in circles - npc.goto = Some(( - npc.wpos - + Vec3::new( - ctx.event.time.0.sin() as f32 * 16.0, - ctx.event.time.0.cos() as f32 * 16.0, - 0.0, - ), - 0.7, - )); - } + }; + + (controller, task_state) + }; + + ctx.state.data_mut().npcs[npc_id].goto = controller.goto; + ctx.state.data_mut().npcs[npc_id].task_state = Some(task_state); } }); Ok(Self) } } + +#[derive(Clone)] +pub struct Generate(F, PhantomData); + +impl PartialEq for Generate { + fn eq(&self, _: &Self) -> bool { true } +} + +pub fn generate(f: F) -> Generate { Generate(f, PhantomData) } + +impl Task for Generate + where F: Clone + Send + Sync + 'static + for<'a> Fn(&T::Ctx<'a>) -> T +{ + type State = (T::State, T); + type Ctx<'a> = T::Ctx<'a>; + + fn begin<'a>(&self, ctx: &Self::Ctx<'a>) -> Self::State { + let task = (self.0)(ctx); + (task.begin(ctx), task) + } + + fn run<'a>( + &self, + (state, task): &mut Self::State, + ctx: &Self::Ctx<'a>, + controller: &mut Controller, + ) -> ControlFlow<()> { + task.run(state, ctx, controller) + } +} + +#[derive(Clone, PartialEq)] +struct Goto(Vec2, f32); + +impl Task for Goto { + type State = (Vec2, f32); + type Ctx<'a> = (&'a Npc, &'a EventCtx<'a, NpcAi, OnTick>); + + fn begin<'a>(&self, (_npc, _ctx): &Self::Ctx<'a>) -> Self::State { (self.0, self.1) } + + fn run<'a>( + &self, + (tgt, speed_factor): &mut Self::State, + (npc, ctx): &Self::Ctx<'a>, + controller: &mut Controller, + ) -> ControlFlow<()> { + if npc.wpos.xy().distance_squared(*tgt) < 2f32.powi(2) { + controller.goto = None; + FINISH + } else { + let dist = npc.wpos.xy().distance(*tgt); + let step = dist.min(32.0); + let next_tgt = npc.wpos.xy() + (*tgt - npc.wpos.xy()) / dist * step; + + if npc.goto.map_or(true, |(tgt, _)| tgt.xy().distance_squared(next_tgt) > (step * 0.5).powi(2)) || npc.wpos.xy().distance_squared(next_tgt) < (step * 0.5).powi(2) { + controller.goto = Some((next_tgt.with_z(ctx.world.sim().get_alt_approx(next_tgt.map(|e| e as i32)).unwrap_or(0.0)), *speed_factor)); + } + CONTINUE + } + } +} + +#[derive(Clone, PartialEq)] +struct TravelToSite(SiteId); + +impl Task for TravelToSite { + type State = (PathingMemory, TaskState); + type Ctx<'a> = (&'a Npc, &'a EventCtx<'a, NpcAi, OnTick>); + + fn begin<'a>(&self, (npc, ctx): &Self::Ctx<'a>) -> Self::State { + (PathingMemory::default(), TaskState::default()) + } + + fn run<'a>( + &self, + (pathing, task_state): &mut Self::State, + (npc, ctx): &Self::Ctx<'a>, + controller: &mut Controller, + ) -> ControlFlow<()> { + if let Some(current_site) = npc.current_site { + if pathing.intersite_path.is_none() { + pathing.intersite_path = path_towns( + current_site, + self.0, + &ctx.state.data().sites, + ctx.world, + ); + if pathing.intersite_path.is_none() { + return FINISH; + } + } + } + + if let Some((ref mut path, site)) = pathing.intrasite_path { + // If the npc walking in a site and want to reroll (because the path was + // exhausted.) to try to find a complete path. + if path.repoll { + pathing.intrasite_path = + path_town(npc.wpos, site, ctx.index, |_| Some(path.end)) + .map(|path| (path, site)); + } + } + + if let Some((ref mut path, site)) = pathing.intrasite_path { + if let Some(next_tile) = path.path.front() { + match &ctx.index.sites.get(site).kind { + SiteKind::Refactor(site) + | SiteKind::CliffTown(site) + | SiteKind::DesertCity(site) => { + // Set the target to the next node in the path. + let wpos = site.tile_center_wpos(*next_tile); + task_state.perform(Goto(wpos.map(|e| e as f32 + 0.5), 1.0), &(npc, ctx), controller)?; + path.path.pop_front(); + return CONTINUE; + }, + _ => {}, + } + } else { + // If the path is empty, we're done. + pathing.intrasite_path = None; + } + } + + if let Some((path, progress)) = { + if let Some((path, progress)) = &mut pathing.intersite_path { + if let Some((track_id, _)) = path.path.front() { + let track = ctx.world.civs().tracks.get(*track_id); + if *progress >= track.path().len() { + if path.repoll { + // Repoll if last path wasn't complete. + pathing.intersite_path = path_towns( + npc.current_site.unwrap(), + path.end, + &ctx.state.data().sites, + ctx.world, + ); + } else { + // Otherwise just take the next in the calculated path. + path.path.pop_front(); + *progress = 0; + } + } + } + } + &mut pathing.intersite_path + } { + if let Some((track_id, reversed)) = path.path.front() { + let track = ctx.world.civs().tracks.get(*track_id); + let get_progress = |progress: usize| { + if *reversed { + track.path().len().wrapping_sub(progress + 1) + } else { + progress + } + }; + + let transform_path_pos = |chunk_pos| { + let chunk_wpos = TerrainChunkSize::center_wpos(chunk_pos); + if let Some(pathdata) = + ctx.world.sim().get_nearest_path(chunk_wpos) + { + pathdata.1.map(|e| e as i32) + } else { + chunk_wpos + } + }; + + // Loop through and skip nodes that are inside a site, and use intra + // site path finding there instead. + let walk_path = if let Some(chunk_pos) = + track.path().nodes.get(get_progress(*progress)) + { + if let Some((wpos, site_id, site)) = + ctx.world.sim().get(*chunk_pos).and_then(|chunk| { + let site_id = *chunk.sites.first()?; + let wpos = transform_path_pos(*chunk_pos); + match &ctx.index.sites.get(site_id).kind { + SiteKind::Refactor(site) + | SiteKind::CliffTown(site) + | SiteKind::DesertCity(site) => { + Some((wpos, site_id, site)) + }, + _ => None, + } + }) + { + if pathing.intrasite_path.is_none() { + let end = site.wpos_tile_pos(wpos); + pathing.intrasite_path = + path_town(npc.wpos, site_id, ctx.index, |_| { + Some(end) + }) + .map(|path| (path, site_id)); + } + if site.wpos_tile(wpos).is_obstacle() { + *progress += 1; + pathing.intrasite_path = None; + false + } else { + true + } + } else { + true + } + } else { + false + }; + + if walk_path { + // Find the next wpos on the path. + // NOTE: Consider not having this big gap between current + // position and next. For better path finding. Maybe that would + // mean having a float for progress. + let wpos = transform_path_pos( + track.path().nodes[get_progress(*progress)], + ); + task_state.perform(Goto(wpos.map(|e| e as f32 + 0.5), 0.8), &(npc, ctx), controller)?; + *progress += 1; + return CONTINUE; + } + } else { + pathing.intersite_path = None; + } + } + + let world_site = |site_id: SiteId| { + let id = ctx.state.data().sites.get(site_id).and_then(|site| site.world_site)?; + ctx.world.civs().sites.recreate_id(id.id()) + }; + + if let Some(site_wpos) = world_site(self.0) + .map(|home| TerrainChunkSize::center_wpos(ctx.world.civs().sites.get(home).center)) + { + if site_wpos.map(|e| e as f32 + 0.5).distance_squared(npc.wpos.xy()) < 16f32.powi(2) { + FINISH + } else { + task_state.perform(Goto(site_wpos.map(|e| e as f32 + 0.5), 0.8), &(npc, ctx), controller) + } + } else { + FINISH + } + } +} diff --git a/rtsim/src/rule/simulate_npcs.rs b/rtsim/src/rule/simulate_npcs.rs index b2761cc404..06a816b6dd 100644 --- a/rtsim/src/rule/simulate_npcs.rs +++ b/rtsim/src/rule/simulate_npcs.rs @@ -36,6 +36,13 @@ impl Rule for SimulateNpcs { .sim() .get_alt_approx(npc.wpos.xy().map(|e| e as i32)) .unwrap_or(0.0); + + // Update the NPC's current site, if any + npc.current_site = ctx + .world + .sim() + .get(npc.wpos.xy().as_::() / TerrainChunkSize::RECT_SIZE.as_()) + .and_then(|chunk| data.sites.world_site_map.get(chunk.sites.first()?).copied()); } }); diff --git a/server/src/rtsim2/tick.rs b/server/src/rtsim2/tick.rs index bc794b7722..ff8435f711 100644 --- a/server/src/rtsim2/tick.rs +++ b/server/src/rtsim2/tick.rs @@ -260,16 +260,17 @@ impl<'a> System<'a> for Sys { 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); - agent.rtsim_controller.heading_to = - npc.pathing.intersite_path.as_ref().and_then(|(path, _)| { - Some( - index - .sites - .get(data.sites.get(path.end)?.world_site?) - .name() - .to_string(), - ) - }); + // TODO: + // agent.rtsim_controller.heading_to = + // npc.pathing.intersite_path.as_ref().and_then(|(path, _)| { + // Some( + // index + // .sites + // .get(data.sites.get(path.end)?.world_site?) + // .name() + // .to_string(), + // ) + // }); } }); }