Allow NPCs to migrate away from towns with a high population density

This commit is contained in:
Joshua Barretto 2023-04-13 12:00:59 +01:00
parent 9e17042bf6
commit 2a1ea63910
4 changed files with 157 additions and 125 deletions

View File

@ -225,6 +225,9 @@ npc-speech-prisoner =
.a4 = I wish i still had my pick!
npc-speech-moving_on =
.a0 = I've spent enough time here, onward to { $site }!
npc-speech-migrating =
.a0 = I'm no longer happy living here. Time to migrate to { $site }.
.a1 = Time to move to { $site }, I've had it with this place.
npc-speech-night_time =
.a0 = It's dark, time to head home.
.a1 = I'm tired.

View File

@ -55,6 +55,7 @@ pub struct PathingMemory {
pub struct Controller {
pub actions: Vec<NpcAction>,
pub activity: Option<NpcActivity>,
pub new_home: Option<SiteId>,
}
impl Controller {
@ -79,6 +80,8 @@ impl Controller {
pub fn attack(&mut self, target: impl Into<Actor>) {
self.actions.push(NpcAction::Attack(target.into()));
}
pub fn set_new_home(&mut self, new_home: SiteId) { self.new_home = Some(new_home); }
}
pub struct Brain {

View File

@ -599,28 +599,50 @@ fn choose_plaza(ctx: &mut NpcCtx, site: SiteId) -> Option<Vec2<f32>> {
fn villager(visiting_site: SiteId) -> impl Action {
choose(move |ctx| {
/*
if ctx
.state
.data()
.sites
.get(visiting_site)
.map_or(true, |s| s.world_site.is_none())
// Consider moving home if the home site gets too full
if ctx.rng.gen_bool(0.0001)
&& let Some(home) = ctx.npc.home
&& Some(home) == ctx.npc.current_site
&& let Some(home_pop_ratio) = ctx.state.data().sites.get(home)
.and_then(|site| Some((site, ctx.index.sites.get(site.world_site?).site2()?)))
.map(|(site, site2)| site.population.len() as f32 / site2.plots().len() as f32)
// Only consider moving if the population is more than 1.5x the number of homes
.filter(|pop_ratio| *pop_ratio > 1.5)
&& let Some(new_home) = ctx
.state
.data()
.sites
.iter()
// Don't try to move to the site that's currently our home
.filter(|(site_id, _)| Some(*site_id) != ctx.npc.home)
// Only consider towns as potential homes
.filter_map(|(site_id, site)| {
let site2 = match site.world_site.map(|ws| &ctx.index.sites.get(ws).kind) {
Some(SiteKind::Refactor(site2)
| SiteKind::CliffTown(site2)
| SiteKind::SavannahPit(site2)
| SiteKind::DesertCity(site2)) => site2,
_ => return None,
};
Some((site_id, site, site2))
})
// Only select sites that are less densely populated than our own
.filter(|(_, site, site2)| (site.population.len() as f32 / site2.plots().len() as f32) < home_pop_ratio)
// Find the closest of the candidate sites
.min_by_key(|(_, site, _)| site.wpos.as_().distance(ctx.npc.wpos.xy()) as i32)
.map(|(site_id, _, _)| site_id)
{
return casual(idle()
.debug(|| "idling (visiting site does not exist, perhaps it's stale data?)"));
} else if ctx.npc.current_site != Some(visiting_site) {
let npc_home = ctx.npc.home;
// Travel to the site we're supposed to be in
return urgent(travel_to_site(visiting_site, 1.0).debug(move || {
if npc_home == Some(visiting_site) {
"travel home".to_string()
} else {
"travel to visiting site".to_string()
let site_name = ctx.state.data().sites[new_home].world_site
.map(|ws| ctx.index.sites.get(ws).name().to_string());
return important(just(move |ctx| {
if let Some(site_name) = &site_name {
ctx.controller.say(None, Content::localized_with_args("npc-speech-migrating", [("site", site_name.clone())]))
}
}));
} else
*/
})
.then(travel_to_site(new_home, 0.5))
.then(just(move |ctx| ctx.controller.set_new_home(new_home))));
}
if DayPeriod::from(ctx.time_of_day.0).is_dark()
&& !matches!(ctx.npc.profession, Some(Profession::Guard))
{

View File

@ -151,125 +151,129 @@ fn on_death(ctx: EventCtx<SimulateNpcs, OnDeath>) {
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) && !npc.is_dead)
{
// Simulate NPC movement when riding
if let Some(riding) = &npc.riding {
if let Some(vehicle) = data.npcs.vehicles.get_mut(riding.vehicle) {
for npc in data.npcs.npcs.values_mut().filter(|npc| !npc.is_dead) {
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_().wpos_to_cpos();
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 {
// 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();
// 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) {
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_().wpos_to_cpos();
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;
}
npc.wpos += (diff
* (npc.body.max_speed_approx() * speed_factor * ctx.event.dt
/ dist2.sqrt())
.min(1.0))
.with_z(0.0);
}
},
// When riding, other actions are disabled
Some(
NpcActivity::Goto(_, _)
| NpcActivity::Gather(_)
| NpcActivity::HuntAnimals
| NpcActivity::Dance,
) => {},
NpcActivity::Gather(_) | NpcActivity::HuntAnimals | NpcActivity::Dance,
) => {
// TODO: Maybe they should walk around randomly
// when gathering resources?
},
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::Say(_, _) => {}, // Currently, just swallow interactions
NpcAction::Attack(_) => {}, // TODO: Implement simulated combat
}
}
// 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();
}
// Consume NPC actions
for action in std::mem::take(&mut npc.controller.actions) {
match action {
NpcAction::Say(_, _) => {}, // Currently, just swallow interactions
NpcAction::Attack(_) => {}, // TODO: Implement simulated combat
}
// Move home if required
if let Some(new_home) = npc.controller.new_home.take() {
npc.home = Some(new_home);
}
// 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();
}
}