Made NPCs give directions to nearby towns, fixed player death propagation

This commit is contained in:
Joshua Barretto 2023-04-13 14:34:31 +01:00
parent d26a711469
commit daacadaedb
9 changed files with 207 additions and 110 deletions

View File

@ -250,6 +250,10 @@ npc-speech-merchant_sell_directed =
.a0 = You there! Are you in need of a new thingamabob?
.a1 = Are you hungry? I'm sure I've got some cheese you can buy.
.a2 = You look like you could do with some new armour!
npc-speech-tell_site =
.a0 = Have you visited { $site }? It's just { $dir } of here!
.a1 = You should visit { $site } some time.
.a2 = If you travel { $dir }, you can get to { $site }.
npc-speech-witness_murder =
.a0 = Murderer!
.a1 = How could you do this?
@ -258,3 +262,11 @@ npc-speech-witness_death =
.a0 = No!
.a1 = This is terrible!
.a2 = Oh my goodness!
npc-speech-dir_north = north
npc-speech-dir_north_east = north-east
npc-speech-dir_east = east
npc-speech-dir_south_east = south-east
npc-speech-dir_south = south
npc-speech-dir_south_west = south-west
npc-speech-dir_west = west
npc-speech-dir_north_west = north-west

View File

@ -231,6 +231,10 @@ pub enum LocalizationArg {
Nat(u64),
}
impl From<Content> for LocalizationArg {
fn from(content: Content) -> Self { Self::Content(content) }
}
// TODO: Remove impl and make use of `Content(Plain(...))` explicit (to
// discourage it)
impl From<String> for LocalizationArg {

View File

@ -1,3 +1,4 @@
use super::Content;
use vek::Vec2;
// TODO: Move this to common/src/, it's not a component
@ -39,7 +40,7 @@ impl Direction {
}
}
// TODO: localization
// TODO: Remove this in favour of `Direction::localize_npc`?
pub fn name(&self) -> &'static str {
match self {
Direction::North => "North",
@ -52,6 +53,19 @@ impl Direction {
Direction::Northwest => "Northwest",
}
}
pub fn localize_npc(&self) -> Content {
Content::localized(match self {
Direction::North => "npc-speech-dir_north",
Direction::Northeast => "npc-speech-dir_north_east",
Direction::East => "npc-speech-dir_east",
Direction::Southeast => "npc-speech-dir_south_east",
Direction::South => "npc-speech-dir_south",
Direction::Southwest => "npc-speech-dir_south_west",
Direction::West => "npc-speech-dir_west",
Direction::Northwest => "npc-speech-dir_north_west",
})
}
}
/// Arbitrarily named Distances

View File

@ -40,6 +40,13 @@ pub struct Site {
// Note: there's currently no guarantee that site populations are non-intersecting
#[serde(skip_serializing, skip_deserializing)]
pub population: HashSet<NpcId>,
/// A list of the nearby sites where each elements is both further and
/// larger (currently based on number of plots) than the next.
/// Effectively, this is a list of nearby sites that might be deemed
/// 'important' to the current one
#[serde(skip_serializing, skip_deserializing)]
pub nearby_sites_by_size: Vec<SiteId>,
}
impl Site {

View File

@ -53,6 +53,7 @@ impl Site {
}),
population: Default::default(),
known_reports: Default::default(),
nearby_sites_by_size: Vec::new(),
}
}
}

View File

@ -11,9 +11,9 @@ use crate::{
};
use common::{
astar::{Astar, PathResult},
comp::Content,
comp::{compass::Direction, Content},
path::Path,
rtsim::{ChunkResource, Profession, SiteId},
rtsim::{Actor, ChunkResource, Profession, SiteId},
spiral::Spiral2d,
store::Id,
terrain::{CoordinateConversions, SiteKindMeta, TerrainChunkSize},
@ -489,7 +489,21 @@ fn socialize() -> impl Action {
.choose(&mut ctx.rng)
{
return Either::Left(
just(move |ctx| ctx.controller.say(other, ctx.npc.personality.get_generic_comment(&mut ctx.rng)))
just(move |ctx| ctx.controller.say(other, if ctx.rng.gen_bool(0.3)
&& let Some(current_site) = ctx.npc.current_site
&& let Some(current_site) = ctx.state.data().sites.get(current_site)
&& let Some(mention_site) = current_site.nearby_sites_by_size.choose(&mut ctx.rng)
&& let Some(mention_site) = ctx.state.data().sites.get(*mention_site)
&& let Some(mention_site_name) = mention_site.world_site
.map(|ws| ctx.index.sites.get(ws).name().to_string())
{
Content::localized_with_args("npc-speech-tell_site", [
("site", Content::Plain(mention_site_name.clone())),
("dir", Direction::from_dir(mention_site.wpos.as_() - ctx.npc.wpos.xy()).localize_npc()),
])
} else {
ctx.npc.personality.get_generic_comment(&mut ctx.rng)
}))
// After greeting the actor, wait for a while
.then(idle().repeat().stop_if(timeout(4.0)))
.map(|_| ())
@ -713,7 +727,7 @@ fn villager(visiting_site: SiteId) -> impl Action {
.map(|_| ()),
);
}
} else if matches!(ctx.npc.profession, Some(Profession::Guard)) && ctx.rng.gen_bool(0.5) {
} else if matches!(ctx.npc.profession, Some(Profession::Guard)) && ctx.rng.gen_bool(0.7) {
if let Some(plaza_wpos) = choose_plaza(ctx, visiting_site) {
return casual(
travel_to_point(plaza_wpos, 0.4)
@ -918,8 +932,13 @@ fn check_inbox(ctx: &mut NpcCtx) -> Option<impl Action> {
// TODO: Sentiment should be positive if we didn't like actor that died
// TODO: Don't report self
let phrase = if let Some(killer) = killer {
// TODO: For now, we don't make sentiment changes if the killer was an
// NPC because NPCs can't hurt one-another.
// This should be changed in the future.
if !matches!(killer, Actor::Npc(_)) {
// TODO: Don't hard-code sentiment change
ctx.sentiments.change_by(killer, -0.7, Sentiment::VILLAIN);
}
"npc-speech-witness_murder"
} else {
"npc-speech-witness_death"

View File

@ -45,9 +45,8 @@ fn on_setup(ctx: EventCtx<SimulateNpcs, OnSetup>) {
fn on_death(ctx: EventCtx<SimulateNpcs, OnDeath>) {
let data = &mut *ctx.state.data_mut();
if let Actor::Npc(npc_id) = ctx.event.actor
&& let Some(npc) = data.npcs.get(npc_id)
{
if let Actor::Npc(npc_id) = ctx.event.actor {
if let Some(npc) = data.npcs.get(npc_id) {
let mut rng = ChaChaRng::from_seed(thread_rng().gen::<[u8; 32]>());
// Respawn dead NPCs
@ -60,10 +59,13 @@ fn on_death(ctx: EventCtx<SimulateNpcs, OnDeath>) {
Some(*id) != npc.home
&& (npc.faction.is_none() || site.faction == npc.faction)
&& site.world_site.map_or(false, |s| {
matches!(ctx.index.sites.get(s).kind, SiteKind::Refactor(_)
matches!(
ctx.index.sites.get(s).kind,
SiteKind::Refactor(_)
| SiteKind::CliffTown(_)
| SiteKind::SavannahPit(_)
| SiteKind::DesertCity(_))
| SiteKind::DesertCity(_)
)
})
})
.min_by_key(|(_, site)| site.population.len())
@ -148,6 +150,7 @@ fn on_death(ctx: EventCtx<SimulateNpcs, OnDeath>) {
error!("Trying to respawn non-existent NPC");
}
}
}
fn on_tick(ctx: EventCtx<SimulateNpcs, OnTick>) {
let data = &mut *ctx.state.data_mut();

View File

@ -28,6 +28,43 @@ fn on_setup(ctx: EventCtx<SyncNpcs, OnSetup>) {
home.population.insert(npc_id);
}
}
// Update the list of nearest sites by size for each site
let sites_iter = data.sites.iter().filter_map(|(site_id, site)| {
let site2 = site
.world_site
.and_then(|ws| ctx.index.sites.get(ws).site2())?;
Some((site_id, site, site2))
});
let nearest_by_size = sites_iter.clone()
.map(|(site_id, site, site2)| {
let mut other_sites = sites_iter.clone()
// Only include sites in the list if they're not the current one and they're more populus
.filter(|(other_id, _, other_site2)| *other_id != site_id && other_site2.plots().len() > site2.plots().len())
.collect::<Vec<_>>();
other_sites.sort_by_key(|(_, other, _)| other.wpos.distance_squared(site.wpos) as i64);
let mut max_size = 0;
// Remove sites that aren't in increasing order of size (Stalin sort?!)
other_sites.retain(|(_, _, other_site2)| {
if other_site2.plots().len() > max_size {
max_size = other_site2.plots().len();
true
} else {
false
}
});
let nearest_by_size = other_sites
.into_iter()
.map(|(site_id, _, _)| site_id)
.collect::<Vec<_>>();
(site_id, nearest_by_size)
})
.collect::<Vec<_>>();
for (site_id, nearest_by_size) in nearest_by_size {
if let Some(site) = data.sites.get_mut(site_id) {
site.nearby_sites_by_size = nearest_by_size;
}
}
}
fn on_death(ctx: EventCtx<SyncNpcs, OnDeath>) {

View File

@ -513,7 +513,6 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, last_change: Healt
inventory.damage_items(&ability_map, &msm);
}
if should_delete {
if let Some(actor) = state.entity_as_actor(entity) {
state
.ecs()
@ -542,6 +541,7 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, last_change: Healt
);
}
if should_delete {
if let Err(e) = state.delete_entity_recorded(entity) {
error!(?e, ?entity, "Failed to delete destroyed entity");
}