mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
New behaviour tree system for rtsim2
This commit is contained in:
parent
e8b489a71a
commit
1b439d0897
@ -18,4 +18,4 @@ atomic_refcell = "0.1"
|
|||||||
slotmap = { version = "1.0.6", features = ["serde"] }
|
slotmap = { version = "1.0.6", features = ["serde"] }
|
||||||
rand = { version = "0.8", features = ["small_rng"] }
|
rand = { version = "0.8", features = ["small_rng"] }
|
||||||
fxhash = "0.2.1"
|
fxhash = "0.2.1"
|
||||||
itertools = "0.10.3"
|
itertools = "0.10.3"
|
||||||
|
@ -11,7 +11,8 @@ use serde::{Deserialize, Serialize};
|
|||||||
use slotmap::HopSlotMap;
|
use slotmap::HopSlotMap;
|
||||||
use std::{
|
use std::{
|
||||||
collections::VecDeque,
|
collections::VecDeque,
|
||||||
ops::{Deref, DerefMut},
|
ops::{Deref, DerefMut, ControlFlow},
|
||||||
|
any::Any,
|
||||||
};
|
};
|
||||||
use vek::*;
|
use vek::*;
|
||||||
use world::{civ::Track, site::Site as WorldSite, util::RandomPerm};
|
use world::{civ::Track, site::Site as WorldSite, util::RandomPerm};
|
||||||
@ -38,7 +39,113 @@ pub struct PathingMemory {
|
|||||||
pub intersite_path: Option<(PathData<(Id<Track>, bool), SiteId>, usize)>,
|
pub intersite_path: Option<(PathData<(Id<Track>, bool), SiteId>, usize)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
pub struct Controller {
|
||||||
|
pub goto: Option<(Vec3<f32>, f32)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct TaskState {
|
||||||
|
state: Option<Box<dyn Any + Send + Sync>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<B: Task>(self, other: B) -> Then<Self, B> {
|
||||||
|
Then(self, other)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn repeat(self) -> Repeat<Self> {
|
||||||
|
Repeat(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
|
pub struct Then<A, B>(A, B);
|
||||||
|
|
||||||
|
impl<A: Task, B> Task for Then<A, B>
|
||||||
|
where B: for<'a> Task<Ctx<'a> = A::Ctx<'a>>
|
||||||
|
{
|
||||||
|
type State = Result<A::State, B::State>; // 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>(A);
|
||||||
|
|
||||||
|
impl<A: Task> Task for Repeat<A> {
|
||||||
|
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> = (T, <T as Task>::State);
|
||||||
|
|
||||||
|
let mut state = if let Some(state) = self.state
|
||||||
|
.take()
|
||||||
|
.and_then(|state| state
|
||||||
|
.downcast::<StateOf<T>>()
|
||||||
|
.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 {
|
pub struct Npc {
|
||||||
// Persisted state
|
// Persisted state
|
||||||
/// Represents the location of the NPC.
|
/// Represents the location of the NPC.
|
||||||
@ -50,8 +157,6 @@ pub struct Npc {
|
|||||||
pub faction: Option<FactionId>,
|
pub faction: Option<FactionId>,
|
||||||
|
|
||||||
// Unpersisted state
|
// Unpersisted state
|
||||||
#[serde(skip_serializing, skip_deserializing)]
|
|
||||||
pub pathing: PathingMemory,
|
|
||||||
|
|
||||||
#[serde(skip_serializing, skip_deserializing)]
|
#[serde(skip_serializing, skip_deserializing)]
|
||||||
pub current_site: Option<SiteId>,
|
pub current_site: Option<SiteId>,
|
||||||
@ -66,6 +171,26 @@ pub struct Npc {
|
|||||||
/// should instead be derived from the game.
|
/// should instead be derived from the game.
|
||||||
#[serde(skip_serializing, skip_deserializing)]
|
#[serde(skip_serializing, skip_deserializing)]
|
||||||
pub mode: NpcMode,
|
pub mode: NpcMode,
|
||||||
|
|
||||||
|
#[serde(skip_serializing, skip_deserializing)]
|
||||||
|
pub task_state: Option<TaskState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
impl Npc {
|
||||||
@ -79,10 +204,10 @@ impl Npc {
|
|||||||
profession: None,
|
profession: None,
|
||||||
home: None,
|
home: None,
|
||||||
faction: None,
|
faction: None,
|
||||||
pathing: Default::default(),
|
|
||||||
current_site: None,
|
current_site: None,
|
||||||
goto: None,
|
goto: None,
|
||||||
mode: NpcMode::Simulated,
|
mode: NpcMode::Simulated,
|
||||||
|
task_state: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,13 +77,13 @@ impl Data {
|
|||||||
Npc::new(rng.gen(), rand_wpos(&mut rng))
|
Npc::new(rng.gen(), rand_wpos(&mut rng))
|
||||||
.with_faction(site.faction)
|
.with_faction(site.faction)
|
||||||
.with_home(site_id)
|
.with_home(site_id)
|
||||||
.with_profession(match rng.gen_range(0..20) {
|
.with_profession(match rng.gen_range(0..17) {
|
||||||
0 => Profession::Hunter,
|
// 0 => Profession::Hunter,
|
||||||
1 => Profession::Blacksmith,
|
// 1 => Profession::Blacksmith,
|
||||||
2 => Profession::Chef,
|
// 2 => Profession::Chef,
|
||||||
3 => Profession::Alchemist,
|
// 3 => Profession::Alchemist,
|
||||||
5..=10 => Profession::Farmer,
|
// 5..=10 => Profession::Farmer,
|
||||||
11..=15 => Profession::Guard,
|
// 11..=15 => Profession::Guard,
|
||||||
_ => Profession::Adventurer(rng.gen_range(0..=3)),
|
_ => Profession::Adventurer(rng.gen_range(0..=3)),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
@ -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 data;
|
||||||
pub mod event;
|
pub mod event;
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
use std::{collections::VecDeque, hash::BuildHasherDefault};
|
use std::{collections::VecDeque, hash::BuildHasherDefault};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
data::{npc::PathData, Sites},
|
data::{npc::{PathData, PathingMemory, Npc, Task, TaskState, Controller, CONTINUE, FINISH}, Sites},
|
||||||
event::OnTick,
|
event::OnTick,
|
||||||
RtState, Rule, RuleError,
|
RtState, Rule, RuleError, EventCtx,
|
||||||
};
|
};
|
||||||
use common::{
|
use common::{
|
||||||
astar::{Astar, PathResult},
|
astar::{Astar, PathResult},
|
||||||
@ -15,7 +15,7 @@ use common::{
|
|||||||
};
|
};
|
||||||
use fxhash::FxHasher64;
|
use fxhash::FxHasher64;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use rand::seq::IteratorRandom;
|
use rand::prelude::*;
|
||||||
use vek::*;
|
use vek::*;
|
||||||
use world::{
|
use world::{
|
||||||
civ::{self, Track},
|
civ::{self, Track},
|
||||||
@ -23,6 +23,11 @@ use world::{
|
|||||||
site2::{self, TileKind},
|
site2::{self, TileKind},
|
||||||
IndexRef, World,
|
IndexRef, World,
|
||||||
};
|
};
|
||||||
|
use std::{
|
||||||
|
ops::ControlFlow,
|
||||||
|
marker::PhantomData,
|
||||||
|
any::{Any, TypeId},
|
||||||
|
};
|
||||||
|
|
||||||
pub struct NpcAi;
|
pub struct NpcAi;
|
||||||
|
|
||||||
@ -46,7 +51,7 @@ const CARDINALS: &[Vec2<i32>] = &[
|
|||||||
fn path_in_site(start: Vec2<i32>, end: Vec2<i32>, site: &site2::Site) -> PathResult<Vec2<i32>> {
|
fn path_in_site(start: Vec2<i32>, end: Vec2<i32>, site: &site2::Site) -> PathResult<Vec2<i32>> {
|
||||||
let heuristic = |tile: &Vec2<i32>| tile.as_::<f32>().distance(end.as_());
|
let heuristic = |tile: &Vec2<i32>| tile.as_::<f32>().distance(end.as_());
|
||||||
let mut astar = Astar::new(
|
let mut astar = Astar::new(
|
||||||
100,
|
250,
|
||||||
start,
|
start,
|
||||||
&heuristic,
|
&heuristic,
|
||||||
BuildHasherDefault::<FxHasher64>::default(),
|
BuildHasherDefault::<FxHasher64>::default(),
|
||||||
@ -69,7 +74,7 @@ fn path_in_site(start: Vec2<i32>, end: Vec2<i32>, site: &site2::Site) -> PathRes
|
|||||||
| TileKind::Tower(_)
|
| TileKind::Tower(_)
|
||||||
| TileKind::Keep(_)
|
| TileKind::Keep(_)
|
||||||
| TileKind::Gate
|
| TileKind::Gate
|
||||||
| TileKind::GnarlingFortification => 3.0,
|
| TileKind::GnarlingFortification => 20.0,
|
||||||
};
|
};
|
||||||
let is_door_tile = |plot: Id<site2::Plot>, tile: Vec2<i32>| match site.plot(plot).kind() {
|
let is_door_tile = |plot: Id<site2::Plot>, tile: Vec2<i32>| match site.plot(plot).kind() {
|
||||||
site2::PlotKind::House(house) => house.door_tile == tile,
|
site2::PlotKind::House(house) => house.door_tile == tile,
|
||||||
@ -96,7 +101,7 @@ fn path_in_site(start: Vec2<i32>, end: Vec2<i32>, site: &site2::Site) -> PathRes
|
|||||||
};
|
};
|
||||||
|
|
||||||
astar.poll(
|
astar.poll(
|
||||||
100,
|
250,
|
||||||
heuristic,
|
heuristic,
|
||||||
|&tile| NEIGHBOURS.iter().map(move |c| tile + *c),
|
|&tile| NEIGHBOURS.iter().map(move |c| tile + *c),
|
||||||
transition,
|
transition,
|
||||||
@ -132,7 +137,7 @@ fn path_between_sites(
|
|||||||
let heuristic = |site: &Id<civ::Site>| get_site(site).center.as_().distance(end_pos);
|
let heuristic = |site: &Id<civ::Site>| get_site(site).center.as_().distance(end_pos);
|
||||||
|
|
||||||
let mut astar = Astar::new(
|
let mut astar = Astar::new(
|
||||||
100,
|
250,
|
||||||
start,
|
start,
|
||||||
heuristic,
|
heuristic,
|
||||||
BuildHasherDefault::<FxHasher64>::default(),
|
BuildHasherDefault::<FxHasher64>::default(),
|
||||||
@ -149,7 +154,7 @@ fn path_between_sites(
|
|||||||
|
|
||||||
let transition = |a: &Id<civ::Site>, b: &Id<civ::Site>| track_between(*a, *b).cost;
|
let transition = |a: &Id<civ::Site>, b: &Id<civ::Site>| 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| {
|
path.map(|path| {
|
||||||
let path = path
|
let path = path
|
||||||
@ -231,216 +236,314 @@ fn path_towns(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MAX_STEP: f32 = 32.0;
|
||||||
|
|
||||||
impl Rule for NpcAi {
|
impl Rule for NpcAi {
|
||||||
fn start(rtstate: &mut RtState) -> Result<Self, RuleError> {
|
fn start(rtstate: &mut RtState) -> Result<Self, RuleError> {
|
||||||
rtstate.bind::<Self, OnTick>(|ctx| {
|
rtstate.bind::<Self, OnTick>(|mut ctx| {
|
||||||
let data = &mut *ctx.state.data_mut();
|
let npc_ids = ctx.state.data().npcs.keys().collect::<Vec<_>>();
|
||||||
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_::<i32>() / TerrainChunkSize::RECT_SIZE.as_())
|
|
||||||
.and_then(|chunk| data.sites.world_site_map.get(chunk.sites.first()?).copied());
|
|
||||||
|
|
||||||
if let Some(home_id) = npc.home {
|
for npc_id in npc_ids {
|
||||||
if let Some((target, _)) = npc.goto {
|
let mut task_state = ctx.state.data_mut().npcs[npc_id].task_state.take().unwrap_or_default();
|
||||||
// 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));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some((ref mut path, site)) = npc.pathing.intrasite_path {
|
let (controller, task_state) = {
|
||||||
// If the npc walking in a site and want to reroll (because the path was
|
let data = &*ctx.state.data();
|
||||||
// exhausted.) to try to find a complete path.
|
let npc = &data.npcs[npc_id];
|
||||||
if path.repoll {
|
|
||||||
npc.pathing.intrasite_path =
|
|
||||||
path_town(npc.wpos, site, ctx.index, |_| Some(path.end))
|
|
||||||
.map(|path| (path, site));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some((ref mut path, site)) = npc.pathing.intrasite_path {
|
let mut controller = Controller { goto: npc.goto };
|
||||||
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_::<f32>().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)));
|
let action: ControlFlow<()> = try {
|
||||||
},
|
if matches!(npc.profession, Some(Profession::Adventurer(_))) {
|
||||||
_ => {},
|
if let Some(home) = npc.home {
|
||||||
}
|
// Travel between random nearby sites
|
||||||
} else {
|
let task = generate(move |(npc, ctx): &(&Npc, &EventCtx<_, _>)| {
|
||||||
// If the path is empty, we're done.
|
let tgt_site = ctx.state.data().sites
|
||||||
npc.pathing.intrasite_path = None;
|
.iter()
|
||||||
}
|
.filter(|(site_id, site)| npc
|
||||||
} else if let Some((path, progress)) = {
|
.current_site
|
||||||
// Check if we are done with this part of the inter site path.
|
.map_or(true, |cs| *site_id != cs) && thread_rng().gen_bool(0.25))
|
||||||
if let Some((path, progress)) = &mut npc.pathing.intersite_path {
|
.min_by_key(|(_, site)| site.wpos.as_().distance(npc.wpos.xy()) as i32)
|
||||||
if let Some((track_id, _)) = path.path.front() {
|
.map(|(site_id, _)| site_id)
|
||||||
let track = ctx.world.civs().tracks.get(*track_id);
|
.unwrap_or(home);
|
||||||
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 transform_path_pos = |chunk_pos| {
|
TravelToSite(tgt_site)
|
||||||
let chunk_wpos = TerrainChunkSize::center_wpos(chunk_pos);
|
})
|
||||||
if let Some(pathdata) =
|
.repeat();
|
||||||
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
|
task_state.perform(task, &(&*npc, &ctx), &mut controller)?;
|
||||||
// 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_::<f32>().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;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if matches!(npc.profession, Some(Profession::Adventurer(_))) {
|
controller.goto = None;
|
||||||
// If the npc is home, choose a random site to go to, otherwise go
|
|
||||||
// home.
|
// // Choose a random plaza in the npcs home site (which should be the
|
||||||
if let Some(start) = npc.current_site {
|
// // current here) to go to.
|
||||||
let end = if home_id == start {
|
// if let Some(home_id) =
|
||||||
data.sites
|
// data.sites.get(home_id).and_then(|site| site.world_site)
|
||||||
.keys()
|
// {
|
||||||
.filter(|site| *site != home_id)
|
// npc.pathing.intrasite_path =
|
||||||
.choose(&mut dynamic_rng)
|
// path_town(npc.wpos, home_id, ctx.index, |site| {
|
||||||
.unwrap_or(home_id)
|
// Some(
|
||||||
} else {
|
// site.plots
|
||||||
home_id
|
// [site.plazas().choose(&mut dynamic_rng)?]
|
||||||
};
|
// .root_tile(),
|
||||||
npc.pathing.intersite_path =
|
// )
|
||||||
path_towns(start, end, &data.sites, ctx.world);
|
// })
|
||||||
}
|
// .map(|path| (path, home_id));
|
||||||
} 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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
} else {
|
|
||||||
// TODO: Don't make homeless people walk around in circles
|
(controller, task_state)
|
||||||
npc.goto = Some((
|
};
|
||||||
npc.wpos
|
|
||||||
+ Vec3::new(
|
ctx.state.data_mut().npcs[npc_id].goto = controller.goto;
|
||||||
ctx.event.time.0.sin() as f32 * 16.0,
|
ctx.state.data_mut().npcs[npc_id].task_state = Some(task_state);
|
||||||
ctx.event.time.0.cos() as f32 * 16.0,
|
|
||||||
0.0,
|
|
||||||
),
|
|
||||||
0.7,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(Self)
|
Ok(Self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Generate<F, T>(F, PhantomData<T>);
|
||||||
|
|
||||||
|
impl<F, T> PartialEq for Generate<F, T> {
|
||||||
|
fn eq(&self, _: &Self) -> bool { true }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate<F, T>(f: F) -> Generate<F, T> { Generate(f, PhantomData) }
|
||||||
|
|
||||||
|
impl<F, T: Task> Task for Generate<F, T>
|
||||||
|
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>, f32);
|
||||||
|
|
||||||
|
impl Task for Goto {
|
||||||
|
type State = (Vec2<f32>, 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -36,6 +36,13 @@ impl Rule for SimulateNpcs {
|
|||||||
.sim()
|
.sim()
|
||||||
.get_alt_approx(npc.wpos.xy().map(|e| e as i32))
|
.get_alt_approx(npc.wpos.xy().map(|e| e as i32))
|
||||||
.unwrap_or(0.0);
|
.unwrap_or(0.0);
|
||||||
|
|
||||||
|
// 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| data.sites.world_site_map.get(chunk.sites.first()?).copied());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -260,16 +260,17 @@ impl<'a> System<'a> for Sys {
|
|||||||
if let Some(agent) = agent {
|
if let Some(agent) = agent {
|
||||||
agent.rtsim_controller.travel_to = npc.goto.map(|(wpos, _)| wpos);
|
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.speed_factor = npc.goto.map_or(1.0, |(_, sf)| sf);
|
||||||
agent.rtsim_controller.heading_to =
|
// TODO:
|
||||||
npc.pathing.intersite_path.as_ref().and_then(|(path, _)| {
|
// agent.rtsim_controller.heading_to =
|
||||||
Some(
|
// npc.pathing.intersite_path.as_ref().and_then(|(path, _)| {
|
||||||
index
|
// Some(
|
||||||
.sites
|
// index
|
||||||
.get(data.sites.get(path.end)?.world_site?)
|
// .sites
|
||||||
.name()
|
// .get(data.sites.get(path.end)?.world_site?)
|
||||||
.to_string(),
|
// .name()
|
||||||
)
|
// .to_string(),
|
||||||
});
|
// )
|
||||||
|
// });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user