mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Merge branch 'zesterer/towns' into 'master'
Improvements to towns and NPCs See merge request veloren/veloren!3867
This commit is contained in:
commit
504ea158d2
@ -39,6 +39,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- NPCs now have unique names
|
- NPCs now have unique names
|
||||||
- A /scale command that can be used to change the in-game scale of players
|
- A /scale command that can be used to change the in-game scale of players
|
||||||
- Merchants will flog their wares in towns, encouraging nearby character to buy goods from them
|
- Merchants will flog their wares in towns, encouraging nearby character to buy goods from them
|
||||||
|
- NPCs will now tell you about nearby towns and how to visit them
|
||||||
|
- NPCs will migrate to new towns if they are dissatisfied with their current town
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
@ -50,6 +52,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Rescaling of images for the UI is now done when sampling from them on the GPU. Improvements are
|
- Rescaling of images for the UI is now done when sampling from them on the GPU. Improvements are
|
||||||
particularily noticeable when opening the map screen (which involves rescaling a few large
|
particularily noticeable when opening the map screen (which involves rescaling a few large
|
||||||
images) and also when using the voxel minimap view (where a medium size image is updated often).
|
images) and also when using the voxel minimap view (where a medium size image is updated often).
|
||||||
|
- Towns now have a variety of sizes
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
|
@ -23,4 +23,6 @@
|
|||||||
(1, Item("common.items.lantern.geode_purp")),
|
(1, Item("common.items.lantern.geode_purp")),
|
||||||
(1, Item("common.items.boss_drops.lantern")),
|
(1, Item("common.items.boss_drops.lantern")),
|
||||||
]),
|
]),
|
||||||
|
// TODO: Figure out what to do when airship captains die instead of this
|
||||||
|
tabard: Item("common.items.debug.admin"),
|
||||||
)
|
)
|
||||||
|
@ -225,6 +225,9 @@ npc-speech-prisoner =
|
|||||||
.a4 = I wish i still had my pick!
|
.a4 = I wish i still had my pick!
|
||||||
npc-speech-moving_on =
|
npc-speech-moving_on =
|
||||||
.a0 = I've spent enough time here, onward to { $site }!
|
.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 =
|
npc-speech-night_time =
|
||||||
.a0 = It's dark, time to head home.
|
.a0 = It's dark, time to head home.
|
||||||
.a1 = I'm tired.
|
.a1 = I'm tired.
|
||||||
@ -247,6 +250,10 @@ npc-speech-merchant_sell_directed =
|
|||||||
.a0 = You there! Are you in need of a new thingamabob?
|
.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.
|
.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!
|
.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 =
|
npc-speech-witness_murder =
|
||||||
.a0 = Murderer!
|
.a0 = Murderer!
|
||||||
.a1 = How could you do this?
|
.a1 = How could you do this?
|
||||||
@ -255,3 +262,11 @@ npc-speech-witness_death =
|
|||||||
.a0 = No!
|
.a0 = No!
|
||||||
.a1 = This is terrible!
|
.a1 = This is terrible!
|
||||||
.a2 = Oh my goodness!
|
.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
|
||||||
|
@ -218,6 +218,7 @@ pub struct Client {
|
|||||||
player_list: HashMap<Uid, PlayerInfo>,
|
player_list: HashMap<Uid, PlayerInfo>,
|
||||||
character_list: CharacterList,
|
character_list: CharacterList,
|
||||||
sites: HashMap<SiteId, SiteInfoRich>,
|
sites: HashMap<SiteId, SiteInfoRich>,
|
||||||
|
possible_starting_sites: Vec<SiteId>,
|
||||||
pois: Vec<PoiInfo>,
|
pois: Vec<PoiInfo>,
|
||||||
pub chat_mode: ChatMode,
|
pub chat_mode: ChatMode,
|
||||||
recipe_book: RecipeBook,
|
recipe_book: RecipeBook,
|
||||||
@ -654,6 +655,7 @@ impl Client {
|
|||||||
Grid::from_raw(map_size.map(|e| e as i32), lod_horizon),
|
Grid::from_raw(map_size.map(|e| e as i32), lod_horizon),
|
||||||
(world_map_layers, map_size, map_bounds),
|
(world_map_layers, map_size, map_bounds),
|
||||||
world_map.sites,
|
world_map.sites,
|
||||||
|
world_map.possible_starting_sites,
|
||||||
world_map.pois,
|
world_map.pois,
|
||||||
recipe_book,
|
recipe_book,
|
||||||
component_recipe_book,
|
component_recipe_book,
|
||||||
@ -670,6 +672,7 @@ impl Client {
|
|||||||
lod_horizon,
|
lod_horizon,
|
||||||
world_map,
|
world_map,
|
||||||
sites,
|
sites,
|
||||||
|
possible_starting_sites,
|
||||||
pois,
|
pois,
|
||||||
recipe_book,
|
recipe_book,
|
||||||
component_recipe_book,
|
component_recipe_book,
|
||||||
@ -709,6 +712,7 @@ impl Client {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
|
possible_starting_sites,
|
||||||
pois,
|
pois,
|
||||||
recipe_book,
|
recipe_book,
|
||||||
component_recipe_book,
|
component_recipe_book,
|
||||||
@ -1348,6 +1352,8 @@ impl Client {
|
|||||||
/// Unstable, likely to be removed in a future release
|
/// Unstable, likely to be removed in a future release
|
||||||
pub fn sites(&self) -> &HashMap<SiteId, SiteInfoRich> { &self.sites }
|
pub fn sites(&self) -> &HashMap<SiteId, SiteInfoRich> { &self.sites }
|
||||||
|
|
||||||
|
pub fn possible_starting_sites(&self) -> &[SiteId] { &self.possible_starting_sites }
|
||||||
|
|
||||||
/// Unstable, likely to be removed in a future release
|
/// Unstable, likely to be removed in a future release
|
||||||
pub fn pois(&self) -> &Vec<PoiInfo> { &self.pois }
|
pub fn pois(&self) -> &Vec<PoiInfo> { &self.pois }
|
||||||
|
|
||||||
|
@ -121,6 +121,7 @@ pub struct WorldMapMsg {
|
|||||||
/// (256 possible angles).
|
/// (256 possible angles).
|
||||||
pub horizons: [(Vec<u8>, Vec<u8>); 2],
|
pub horizons: [(Vec<u8>, Vec<u8>); 2],
|
||||||
pub sites: Vec<SiteInfo>,
|
pub sites: Vec<SiteInfo>,
|
||||||
|
pub possible_starting_sites: Vec<SiteId>,
|
||||||
pub pois: Vec<PoiInfo>,
|
pub pois: Vec<PoiInfo>,
|
||||||
/// Default chunk (representing the ocean outside the map bounds). Sea
|
/// Default chunk (representing the ocean outside the map bounds). Sea
|
||||||
/// level (used to provide a base altitude) is the lower bound of this
|
/// level (used to provide a base altitude) is the lower bound of this
|
||||||
|
@ -231,6 +231,10 @@ pub enum LocalizationArg {
|
|||||||
Nat(u64),
|
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
|
// TODO: Remove impl and make use of `Content(Plain(...))` explicit (to
|
||||||
// discourage it)
|
// discourage it)
|
||||||
impl From<String> for LocalizationArg {
|
impl From<String> for LocalizationArg {
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
use super::Content;
|
||||||
use vek::Vec2;
|
use vek::Vec2;
|
||||||
// TODO: Move this to common/src/, it's not a component
|
// 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 {
|
pub fn name(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Direction::North => "North",
|
Direction::North => "North",
|
||||||
@ -52,6 +53,19 @@ impl Direction {
|
|||||||
Direction::Northwest => "Northwest",
|
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
|
/// Arbitrarily named Distances
|
||||||
|
@ -29,7 +29,7 @@ use std::{
|
|||||||
/// Note that this number does *not* need incrementing on every change: most
|
/// Note that this number does *not* need incrementing on every change: most
|
||||||
/// field removals/additions are fine. This number should only be incremented
|
/// field removals/additions are fine. This number should only be incremented
|
||||||
/// when we wish to perform a *hard purge* of rtsim data.
|
/// when we wish to perform a *hard purge* of rtsim data.
|
||||||
pub const CURRENT_VERSION: u32 = 0;
|
pub const CURRENT_VERSION: u32 = 1;
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
pub struct Data {
|
pub struct Data {
|
||||||
|
@ -55,6 +55,7 @@ pub struct PathingMemory {
|
|||||||
pub struct Controller {
|
pub struct Controller {
|
||||||
pub actions: Vec<NpcAction>,
|
pub actions: Vec<NpcAction>,
|
||||||
pub activity: Option<NpcActivity>,
|
pub activity: Option<NpcActivity>,
|
||||||
|
pub new_home: Option<SiteId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Controller {
|
impl Controller {
|
||||||
@ -79,6 +80,8 @@ impl Controller {
|
|||||||
pub fn attack(&mut self, target: impl Into<Actor>) {
|
pub fn attack(&mut self, target: impl Into<Actor>) {
|
||||||
self.actions.push(NpcAction::Attack(target.into()));
|
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 {
|
pub struct Brain {
|
||||||
|
@ -40,6 +40,13 @@ pub struct Site {
|
|||||||
// Note: there's currently no guarantee that site populations are non-intersecting
|
// Note: there's currently no guarantee that site populations are non-intersecting
|
||||||
#[serde(skip_serializing, skip_deserializing)]
|
#[serde(skip_serializing, skip_deserializing)]
|
||||||
pub population: HashSet<NpcId>,
|
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 {
|
impl Site {
|
||||||
|
@ -53,6 +53,7 @@ impl Site {
|
|||||||
}),
|
}),
|
||||||
population: Default::default(),
|
population: Default::default(),
|
||||||
known_reports: Default::default(),
|
known_reports: Default::default(),
|
||||||
|
nearby_sites_by_size: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,9 +11,9 @@ use crate::{
|
|||||||
};
|
};
|
||||||
use common::{
|
use common::{
|
||||||
astar::{Astar, PathResult},
|
astar::{Astar, PathResult},
|
||||||
comp::Content,
|
comp::{compass::Direction, Content},
|
||||||
path::Path,
|
path::Path,
|
||||||
rtsim::{ChunkResource, Profession, SiteId},
|
rtsim::{Actor, ChunkResource, Profession, SiteId},
|
||||||
spiral::Spiral2d,
|
spiral::Spiral2d,
|
||||||
store::Id,
|
store::Id,
|
||||||
terrain::{CoordinateConversions, SiteKindMeta, TerrainChunkSize},
|
terrain::{CoordinateConversions, SiteKindMeta, TerrainChunkSize},
|
||||||
@ -378,6 +378,8 @@ fn travel_to_site(tgt_site: SiteId, speed_factor: f32) -> impl Action {
|
|||||||
now(move |ctx| {
|
now(move |ctx| {
|
||||||
let sites = &ctx.state.data().sites;
|
let sites = &ctx.state.data().sites;
|
||||||
|
|
||||||
|
let site_wpos = sites.get(tgt_site).map(|site| site.wpos.as_());
|
||||||
|
|
||||||
// If we're currently in a site, try to find a path to the target site via
|
// If we're currently in a site, try to find a path to the target site via
|
||||||
// tracks
|
// tracks
|
||||||
if let Some(current_site) = ctx.npc.current_site
|
if let Some(current_site) = ctx.npc.current_site
|
||||||
@ -458,8 +460,11 @@ fn travel_to_site(tgt_site: SiteId, speed_factor: f32) -> impl Action {
|
|||||||
// If we can't find a way to get to the site at all, there's nothing more to be done
|
// If we can't find a way to get to the site at all, there's nothing more to be done
|
||||||
finish().boxed()
|
finish().boxed()
|
||||||
}
|
}
|
||||||
|
// Stop the NPC early if we're near the site to prevent huddling around the centre
|
||||||
|
.stop_if(move |ctx| site_wpos.map_or(false, |site_wpos| ctx.npc.wpos.xy().distance_squared(site_wpos) < 16f32.powi(2)))
|
||||||
})
|
})
|
||||||
.debug(move || format!("travel_to_site {:?}", tgt_site))
|
.debug(move || format!("travel_to_site {:?}", tgt_site))
|
||||||
|
.map(|_| ())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Seconds
|
// Seconds
|
||||||
@ -489,7 +494,21 @@ fn socialize() -> impl Action {
|
|||||||
.choose(&mut ctx.rng)
|
.choose(&mut ctx.rng)
|
||||||
{
|
{
|
||||||
return Either::Left(
|
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)),
|
||||||
|
("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
|
// After greeting the actor, wait for a while
|
||||||
.then(idle().repeat().stop_if(timeout(4.0)))
|
.then(idle().repeat().stop_if(timeout(4.0)))
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
@ -599,28 +618,50 @@ fn choose_plaza(ctx: &mut NpcCtx, site: SiteId) -> Option<Vec2<f32>> {
|
|||||||
|
|
||||||
fn villager(visiting_site: SiteId) -> impl Action {
|
fn villager(visiting_site: SiteId) -> impl Action {
|
||||||
choose(move |ctx| {
|
choose(move |ctx| {
|
||||||
/*
|
// Consider moving home if the home site gets too full
|
||||||
if ctx
|
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
|
.state
|
||||||
.data()
|
.data()
|
||||||
.sites
|
.sites
|
||||||
.get(visiting_site)
|
.iter()
|
||||||
.map_or(true, |s| s.world_site.is_none())
|
// 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()
|
let site_name = ctx.state.data().sites[new_home].world_site
|
||||||
.debug(|| "idling (visiting site does not exist, perhaps it's stale data?)"));
|
.map(|ws| ctx.index.sites.get(ws).name().to_string());
|
||||||
} else if ctx.npc.current_site != Some(visiting_site) {
|
return important(just(move |ctx| {
|
||||||
let npc_home = ctx.npc.home;
|
if let Some(site_name) = &site_name {
|
||||||
// Travel to the site we're supposed to be in
|
ctx.controller.say(None, Content::localized_with_args("npc-speech-migrating", [("site", site_name.clone())]))
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
}));
|
})
|
||||||
} 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()
|
if DayPeriod::from(ctx.time_of_day.0).is_dark()
|
||||||
&& !matches!(ctx.npc.profession, Some(Profession::Guard))
|
&& !matches!(ctx.npc.profession, Some(Profession::Guard))
|
||||||
{
|
{
|
||||||
@ -691,10 +732,10 @@ fn villager(visiting_site: SiteId) -> impl Action {
|
|||||||
.map(|_| ()),
|
.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) {
|
if let Some(plaza_wpos) = choose_plaza(ctx, visiting_site) {
|
||||||
return important(
|
return casual(
|
||||||
travel_to_point(plaza_wpos, 0.45)
|
travel_to_point(plaza_wpos, 0.4)
|
||||||
.debug(|| "patrol")
|
.debug(|| "patrol")
|
||||||
.interrupt_with(|ctx| {
|
.interrupt_with(|ctx| {
|
||||||
if ctx.rng.gen_bool(0.0003) {
|
if ctx.rng.gen_bool(0.0003) {
|
||||||
@ -896,8 +937,13 @@ fn check_inbox(ctx: &mut NpcCtx) -> Option<impl Action> {
|
|||||||
// TODO: Sentiment should be positive if we didn't like actor that died
|
// TODO: Sentiment should be positive if we didn't like actor that died
|
||||||
// TODO: Don't report self
|
// TODO: Don't report self
|
||||||
let phrase = if let Some(killer) = killer {
|
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
|
// TODO: Don't hard-code sentiment change
|
||||||
ctx.sentiments.change_by(killer, -0.7, Sentiment::VILLAIN);
|
ctx.sentiments.change_by(killer, -0.7, Sentiment::VILLAIN);
|
||||||
|
}
|
||||||
"npc-speech-witness_murder"
|
"npc-speech-witness_murder"
|
||||||
} else {
|
} else {
|
||||||
"npc-speech-witness_death"
|
"npc-speech-witness_death"
|
||||||
|
@ -45,9 +45,8 @@ fn on_setup(ctx: EventCtx<SimulateNpcs, OnSetup>) {
|
|||||||
fn on_death(ctx: EventCtx<SimulateNpcs, OnDeath>) {
|
fn on_death(ctx: EventCtx<SimulateNpcs, OnDeath>) {
|
||||||
let data = &mut *ctx.state.data_mut();
|
let data = &mut *ctx.state.data_mut();
|
||||||
|
|
||||||
if let Actor::Npc(npc_id) = ctx.event.actor
|
if let Actor::Npc(npc_id) = ctx.event.actor {
|
||||||
&& let Some(npc) = data.npcs.get(npc_id)
|
if let Some(npc) = data.npcs.get(npc_id) {
|
||||||
{
|
|
||||||
let mut rng = ChaChaRng::from_seed(thread_rng().gen::<[u8; 32]>());
|
let mut rng = ChaChaRng::from_seed(thread_rng().gen::<[u8; 32]>());
|
||||||
|
|
||||||
// Respawn dead NPCs
|
// Respawn dead NPCs
|
||||||
@ -60,10 +59,13 @@ fn on_death(ctx: EventCtx<SimulateNpcs, OnDeath>) {
|
|||||||
Some(*id) != npc.home
|
Some(*id) != npc.home
|
||||||
&& (npc.faction.is_none() || site.faction == npc.faction)
|
&& (npc.faction.is_none() || site.faction == npc.faction)
|
||||||
&& site.world_site.map_or(false, |s| {
|
&& 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::CliffTown(_)
|
||||||
| SiteKind::SavannahPit(_)
|
| SiteKind::SavannahPit(_)
|
||||||
| SiteKind::DesertCity(_))
|
| SiteKind::DesertCity(_)
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.min_by_key(|(_, site)| site.population.len())
|
.min_by_key(|(_, site)| site.population.len())
|
||||||
@ -148,15 +150,12 @@ fn on_death(ctx: EventCtx<SimulateNpcs, OnDeath>) {
|
|||||||
error!("Trying to respawn non-existent NPC");
|
error!("Trying to respawn non-existent NPC");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn on_tick(ctx: EventCtx<SimulateNpcs, OnTick>) {
|
fn on_tick(ctx: EventCtx<SimulateNpcs, OnTick>) {
|
||||||
let data = &mut *ctx.state.data_mut();
|
let data = &mut *ctx.state.data_mut();
|
||||||
for npc in data
|
for (npc_id, npc) in data.npcs.npcs.iter_mut().filter(|(_, npc)| !npc.is_dead) {
|
||||||
.npcs
|
if matches!(npc.mode, SimulationMode::Simulated) {
|
||||||
.npcs
|
|
||||||
.values_mut()
|
|
||||||
.filter(|npc| matches!(npc.mode, SimulationMode::Simulated) && !npc.is_dead)
|
|
||||||
{
|
|
||||||
// Simulate NPC movement when riding
|
// Simulate NPC movement when riding
|
||||||
if let Some(riding) = &npc.riding {
|
if let Some(riding) = &npc.riding {
|
||||||
if let Some(vehicle) = data.npcs.vehicles.get_mut(riding.vehicle) {
|
if let Some(vehicle) = data.npcs.vehicles.get_mut(riding.vehicle) {
|
||||||
@ -248,7 +247,9 @@ fn on_tick(ctx: EventCtx<SimulateNpcs, OnTick>) {
|
|||||||
.with_z(0.0);
|
.with_z(0.0);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Some(NpcActivity::Gather(_) | NpcActivity::HuntAnimals | NpcActivity::Dance) => {
|
Some(
|
||||||
|
NpcActivity::Gather(_) | NpcActivity::HuntAnimals | NpcActivity::Dance,
|
||||||
|
) => {
|
||||||
// TODO: Maybe they should walk around randomly
|
// TODO: Maybe they should walk around randomly
|
||||||
// when gathering resources?
|
// when gathering resources?
|
||||||
},
|
},
|
||||||
@ -272,4 +273,20 @@ fn on_tick(ctx: EventCtx<SimulateNpcs, OnTick>) {
|
|||||||
.unwrap_or(0.0)
|
.unwrap_or(0.0)
|
||||||
+ npc.body.flying_height();
|
+ npc.body.flying_height();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Move home if required
|
||||||
|
if let Some(new_home) = npc.controller.new_home.take() {
|
||||||
|
// Remove the NPC from their old home population
|
||||||
|
if let Some(old_home) = npc.home {
|
||||||
|
if let Some(old_home) = data.sites.get_mut(old_home) {
|
||||||
|
old_home.population.remove(&npc_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Add the NPC to their new home population
|
||||||
|
if let Some(new_home) = data.sites.get_mut(new_home) {
|
||||||
|
new_home.population.insert(npc_id);
|
||||||
|
}
|
||||||
|
npc.home = Some(new_home);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,43 @@ fn on_setup(ctx: EventCtx<SyncNpcs, OnSetup>) {
|
|||||||
home.population.insert(npc_id);
|
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>) {
|
fn on_death(ctx: EventCtx<SyncNpcs, OnDeath>) {
|
||||||
|
@ -513,7 +513,6 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, last_change: Healt
|
|||||||
inventory.damage_items(&ability_map, &msm);
|
inventory.damage_items(&ability_map, &msm);
|
||||||
}
|
}
|
||||||
|
|
||||||
if should_delete {
|
|
||||||
if let Some(actor) = state.entity_as_actor(entity) {
|
if let Some(actor) = state.entity_as_actor(entity) {
|
||||||
state
|
state
|
||||||
.ecs()
|
.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) {
|
if let Err(e) = state.delete_entity_recorded(entity) {
|
||||||
error!(?e, ?entity, "Failed to delete destroyed entity");
|
error!(?e, ?entity, "Failed to delete destroyed entity");
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,7 @@ use common::{
|
|||||||
vol::RectVolSize,
|
vol::RectVolSize,
|
||||||
LoadoutBuilder,
|
LoadoutBuilder,
|
||||||
};
|
};
|
||||||
use common_net::msg::world_msg::{SiteId, SiteInfo, SiteKind};
|
use common_net::msg::world_msg::{SiteId, SiteInfo};
|
||||||
use i18n::{Localization, LocalizationHandle};
|
use i18n::{Localization, LocalizationHandle};
|
||||||
//ImageFrame, Tooltip,
|
//ImageFrame, Tooltip,
|
||||||
use crate::settings::Settings;
|
use crate::settings::Settings;
|
||||||
@ -1978,10 +1978,10 @@ impl CharSelectionUi {
|
|||||||
Arc::clone(client.world_data().topo_map_image()),
|
Arc::clone(client.world_data().topo_map_image()),
|
||||||
Some(default_water_color()),
|
Some(default_water_color()),
|
||||||
)),
|
)),
|
||||||
client.sites()
|
client
|
||||||
.values()
|
.possible_starting_sites()
|
||||||
// TODO: Enforce this server-side and add some way to customise it?
|
.iter()
|
||||||
.filter(|info| matches!(&info.site.kind, SiteKind::Town /*| SiteKind::Castle | SiteKind::Bridge*/))
|
.filter_map(|site_id| client.sites().get(site_id))
|
||||||
.map(|info| info.site.clone())
|
.map(|info| info.site.clone())
|
||||||
.collect(),
|
.collect(),
|
||||||
client.world_data().chunk_size().as_(),
|
client.world_data().chunk_size().as_(),
|
||||||
|
@ -359,11 +359,15 @@ impl Civs {
|
|||||||
SiteKind::Castle => {
|
SiteKind::Castle => {
|
||||||
WorldSite::castle(Castle::generate(wpos, Some(ctx.sim), &mut rng))
|
WorldSite::castle(Castle::generate(wpos, Some(ctx.sim), &mut rng))
|
||||||
},
|
},
|
||||||
SiteKind::Refactor => WorldSite::refactor(site2::Site::generate_city(
|
SiteKind::Refactor => {
|
||||||
|
let size = Lerp::lerp(0.03, 1.0, rng.gen_range(0.0..1f32).powi(5));
|
||||||
|
WorldSite::refactor(site2::Site::generate_city(
|
||||||
&Land::from_sim(ctx.sim),
|
&Land::from_sim(ctx.sim),
|
||||||
&mut rng,
|
&mut rng,
|
||||||
wpos,
|
wpos,
|
||||||
)),
|
size,
|
||||||
|
))
|
||||||
|
},
|
||||||
SiteKind::CliffTown => WorldSite::cliff_town(site2::Site::generate_cliff_town(
|
SiteKind::CliffTown => WorldSite::cliff_town(site2::Site::generate_cliff_town(
|
||||||
&Land::from_sim(ctx.sim),
|
&Land::from_sim(ctx.sim),
|
||||||
&mut rng,
|
&mut rng,
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
)]
|
)]
|
||||||
#![allow(clippy::branches_sharing_code)] // TODO: evaluate
|
#![allow(clippy::branches_sharing_code)] // TODO: evaluate
|
||||||
#![deny(clippy::clone_on_ref_ptr)]
|
#![deny(clippy::clone_on_ref_ptr)]
|
||||||
#![feature(option_zip)]
|
#![feature(option_zip, let_chains)]
|
||||||
|
|
||||||
mod all;
|
mod all;
|
||||||
mod block;
|
mod block;
|
||||||
@ -205,6 +205,37 @@ impl World {
|
|||||||
wpos,
|
wpos,
|
||||||
}))
|
}))
|
||||||
.collect(),
|
.collect(),
|
||||||
|
possible_starting_sites: {
|
||||||
|
const STARTING_SITE_COUNT: usize = 4;
|
||||||
|
|
||||||
|
let mut candidates = self
|
||||||
|
.civs()
|
||||||
|
.sites
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(_, civ_site)| Some((civ_site, civ_site.site_tmp?)))
|
||||||
|
.map(|(civ_site, site_id)| {
|
||||||
|
// Score the site according to how suitable it is to be a starting site
|
||||||
|
let mut score = 0.0;
|
||||||
|
|
||||||
|
if let SiteKind::Refactor(site2) = &index.sites[site_id].kind {
|
||||||
|
// Strongly prefer towns
|
||||||
|
score += 1000.0;
|
||||||
|
// Prefer sites of a medium size
|
||||||
|
score += 2.0 / (1.0 + (site2.plots().len() as f32 - 20.0).abs() / 10.0);
|
||||||
|
};
|
||||||
|
// Prefer sites in hospitable climates
|
||||||
|
if let Some(chunk) = self.sim().get(civ_site.center) {
|
||||||
|
score += 1.0 / (1.0 + chunk.temp.abs());
|
||||||
|
score += 1.0 / (1.0 + (chunk.humidity - CONFIG.forest_hum).abs() * 2.0);
|
||||||
|
}
|
||||||
|
// Prefer sites that are close to the centre of the world
|
||||||
|
score += 4.0 / (1.0 + civ_site.center.map2(self.sim().get_size(), |e, sz| (e as f32 / sz as f32 - 0.5).abs() * 2.0).reduce_partial_max());
|
||||||
|
(site_id.id(), score)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
candidates.sort_by_key(|(_, score)| -(*score * 1000.0) as i32);
|
||||||
|
candidates.into_iter().map(|(site_id, _)| site_id).take(STARTING_SITE_COUNT).collect()
|
||||||
|
},
|
||||||
..self.sim.get_map(index, self.sim().calendar.as_ref())
|
..self.sim.get_map(index, self.sim().calendar.as_ref())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -1683,6 +1683,7 @@ impl WorldSim {
|
|||||||
horizons,
|
horizons,
|
||||||
sites: Vec::new(), // Will be substituted later
|
sites: Vec::new(), // Will be substituted later
|
||||||
pois: Vec::new(), // Will be substituted later
|
pois: Vec::new(), // Will be substituted later
|
||||||
|
possible_starting_sites: Vec::new(), // Will be substituted later
|
||||||
default_chunk: Arc::new(self.generate_oob_chunk()),
|
default_chunk: Arc::new(self.generate_oob_chunk()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -493,7 +493,8 @@ impl Site {
|
|||||||
site
|
site
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_city(land: &Land, rng: &mut impl Rng, origin: Vec2<i32>) -> Self {
|
// Size is 0..1
|
||||||
|
pub fn generate_city(land: &Land, rng: &mut impl Rng, origin: Vec2<i32>, size: f32) -> Self {
|
||||||
let mut rng = reseed(rng);
|
let mut rng = reseed(rng);
|
||||||
|
|
||||||
let mut site = Site {
|
let mut site = Site {
|
||||||
@ -510,7 +511,7 @@ impl Site {
|
|||||||
|
|
||||||
let mut castles = 0;
|
let mut castles = 0;
|
||||||
|
|
||||||
for _ in 0..120 {
|
for _ in 0..(size * 200.0) as i32 {
|
||||||
match *build_chance.choose_seeded(rng.gen()) {
|
match *build_chance.choose_seeded(rng.gen()) {
|
||||||
// House
|
// House
|
||||||
1 => {
|
1 => {
|
||||||
@ -1417,7 +1418,9 @@ impl Site {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn test_site() -> Site { Site::generate_city(&Land::empty(), &mut thread_rng(), Vec2::zero()) }
|
pub fn test_site() -> Site {
|
||||||
|
Site::generate_city(&Land::empty(), &mut thread_rng(), Vec2::zero(), 0.5)
|
||||||
|
}
|
||||||
|
|
||||||
fn wpos_is_hazard(land: &Land, wpos: Vec2<i32>) -> Option<HazardKind> {
|
fn wpos_is_hazard(land: &Land, wpos: Vec2<i32>) -> Option<HazardKind> {
|
||||||
if land
|
if land
|
||||||
|
Loading…
Reference in New Issue
Block a user