More interesting idle behaviours

This commit is contained in:
Joshua Barretto 2023-01-05 13:15:09 +00:00
parent 8f7b11f12f
commit ac83cfc4a3
3 changed files with 104 additions and 36 deletions

View File

@ -65,14 +65,19 @@ impl Data {
);
// Spawn some test entities at the sites
for (site_id, site) in this.sites.iter().take(1) {
for (site_id, site) in this.sites.iter()
// TODO: Stupid
.filter(|(_, site)| site.world_site.map_or(false, |ws| matches!(&index.sites.get(ws).kind, SiteKind::Refactor(_))))
.skip(1)
.take(1)
{
let rand_wpos = |rng: &mut SmallRng| {
let wpos2d = site.wpos.map(|e| e + rng.gen_range(-10..10));
wpos2d
.map(|e| e as f32 + 0.5)
.with_z(world.sim().get_alt_approx(wpos2d).unwrap_or(0.0))
};
for _ in 0..10 {
for _ in 0..16 {
this.npcs.create(
Npc::new(rng.gen(), rand_wpos(&mut rng))
.with_faction(site.faction)

View File

@ -1,7 +1,7 @@
use std::{collections::VecDeque, hash::BuildHasherDefault};
use crate::{
ai::{casual, choose, finish, just, now, seq, urgent, watch, Action, NpcCtx},
ai::{casual, choose, finish, important, just, now, seq, urgent, watch, Action, NpcCtx},
data::{
npc::{Brain, Controller, Npc, NpcId, PathData, PathingMemory},
Sites,
@ -208,24 +208,18 @@ fn path_towns(
end: SiteId,
sites: &Sites,
world: &World,
) -> Option<(PathData<(Id<Track>, bool), SiteId>, usize)> {
) -> Option<PathData<(Id<Track>, bool), SiteId>> {
match path_between_sites(start, end, sites, world) {
PathResult::Exhausted(p) => Some((
PathData {
end,
path: p.nodes.into(),
repoll: true,
},
0,
)),
PathResult::Path(p) => Some((
PathData {
end,
path: p.nodes.into(),
repoll: false,
},
0,
)),
PathResult::Exhausted(p) => Some(PathData {
end,
path: p.nodes.into(),
repoll: true,
}),
PathResult::Path(p) => Some(PathData {
end,
path: p.nodes.into(),
repoll: false,
}),
PathResult::Pending | PathResult::None(_) => None,
}
}
@ -323,16 +317,28 @@ fn idle() -> impl Action { just(|ctx| *ctx.controller = Controller::idle()) }
/// Try to walk toward a 3D position without caring for obstacles.
fn goto(wpos: Vec3<f32>, speed_factor: f32) -> impl Action {
const STEP_DIST: f32 = 16.0;
const STEP_DIST: f32 = 24.0;
const WAYPOINT_DIST: f32 = 12.0;
const GOAL_DIST: f32 = 2.0;
let mut waypoint = None;
just(move |ctx| {
let rpos = wpos - ctx.npc.wpos;
let len = rpos.magnitude();
ctx.controller.goto = Some((
ctx.npc.wpos + (rpos / len) * len.min(STEP_DIST),
speed_factor,
));
// If we're close to the next waypoint, complete it
if waypoint.map_or(false, |waypoint: Vec3<f32>| {
ctx.npc.wpos.xy().distance_squared(waypoint.xy()) < WAYPOINT_DIST.powi(2)
}) {
waypoint = None;
}
// Get the next waypoint on the route toward the goal
let waypoint =
waypoint.get_or_insert_with(|| ctx.npc.wpos + (rpos / len) * len.min(STEP_DIST));
ctx.controller.goto = Some((*waypoint, speed_factor));
})
.repeat()
.stop_if(move |ctx| ctx.npc.wpos.xy().distance_squared(wpos.xy()) < GOAL_DIST.powi(2))
@ -358,7 +364,7 @@ fn travel_to_site(tgt_site: SiteId) -> impl Action {
// If we're currently in a site, try to find a path to the target site via
// tracks
if let Some(current_site) = ctx.npc.current_site
&& let Some((mut tracks, _)) = path_towns(current_site, tgt_site, sites, ctx.world)
&& let Some(tracks) = path_towns(current_site, tgt_site, sites, ctx.world)
{
// For every track in the path we discovered between the sites...
seq(tracks
@ -408,6 +414,53 @@ fn timeout(ctx: &NpcCtx, time: f64) -> impl FnMut(&mut NpcCtx) -> bool + Clone +
move |ctx| ctx.time.0 > end
}
fn villager() -> impl Action {
choose(|ctx| {
if let Some(home) = ctx.npc.home {
if ctx.npc.current_site != Some(home) {
// Travel home if we're not there already
important(travel_to_site(home))
} else if matches!(
ctx.npc.profession,
Some(Profession::Merchant | Profession::Blacksmith)
) {
// Trade professions just walk between town plazas
casual(now(move |ctx| {
// Choose a plaza in the NPC's home site to walk to
if let Some(plaza_wpos) = ctx
.state
.data()
.sites
.get(home)
.and_then(|home| ctx.index.sites.get(home.world_site?).site2())
.and_then(|site2| {
let plaza = &site2.plots[site2.plazas().choose(&mut thread_rng())?];
Some(site2.tile_center_wpos(plaza.root_tile()).as_())
})
{
// Walk to the plaza...
goto_2d(plaza_wpos, 0.5)
// ...then wait for some time before moving on
.then(now(|ctx| {
let wait_time = thread_rng().gen_range(10.0..30.0);
idle().repeat().stop_if(timeout(ctx, wait_time))
}))
.map(|_| ())
.boxed()
} else {
// No plazas? :(
finish().boxed()
}
}))
} else {
casual(idle())
}
} else {
casual(finish()) // Nothing to do if we're homeless!
}
})
}
fn think() -> impl Action {
choose(|ctx| {
if matches!(ctx.npc.profession, Some(Profession::Adventurer(_))) {
@ -431,17 +484,8 @@ fn think() -> impl Action {
} else {
casual(finish())
}
} else if matches!(ctx.npc.profession, Some(Profession::Blacksmith)) {
casual(idle())
} else {
casual(
now(|ctx| goto(ctx.npc.wpos + Vec3::unit_x() * 10.0, 1.0))
.then(now(|ctx| goto(ctx.npc.wpos - Vec3::unit_x() * 10.0, 1.0)))
.repeat()
.stop_if(timeout(ctx, 10.0))
.then(now(|ctx| idle().repeat().stop_if(timeout(ctx, 5.0))))
.map(|_| {}),
)
casual(villager())
}
})
}

View File

@ -305,6 +305,25 @@ impl Site {
| SiteKind::Settlement(_)
)
}
/// Return the inner site2 site, if this site has one.
// TODO: Remove all of this when site1 gets removed.
pub fn site2(&self) -> Option<&site2::Site> {
match &self.kind {
SiteKind::Settlement(_) => None,
SiteKind::Dungeon(site2) => Some(site2),
SiteKind::Castle(_) => None,
SiteKind::Refactor(site2) => Some(site2),
SiteKind::CliffTown(site2) => Some(site2),
SiteKind::SavannahPit(site2) => Some(site2),
SiteKind::Tree(_) => None,
SiteKind::DesertCity(site2) => Some(site2),
SiteKind::ChapelSite(site2) => Some(site2),
SiteKind::GiantTree(site2) => Some(site2),
SiteKind::Gnarling(site2) => Some(site2),
SiteKind::Bridge(site2) => Some(site2),
}
}
}
impl SiteKind {