Merge branch 'zesterer/rtsim-tweaks' into 'master'

Started adding wandering rtsim monsters

See merge request veloren/veloren!3908
This commit is contained in:
Joshua Barretto 2023-05-09 14:51:15 +00:00
commit 1cc221f653
32 changed files with 552 additions and 193 deletions

View File

@ -0,0 +1,11 @@
#![enable(implicit_some)]
(
name: Automatic,
body: RandomWith("cyclops"),
alignment: Alignment(Enemy),
loot: LootTable("common.loot_tables.dungeon.tier-4.miniboss"),
inventory: (
loadout: FromBody,
),
meta: [],
)

View File

@ -0,0 +1,11 @@
#![enable(implicit_some)]
(
name: Automatic,
body: RandomWith("werewolf"),
alignment: Alignment(Enemy),
loot: LootTable("common.loot_tables.creature.biped_large.default"),
inventory: (
loadout: FromBody,
),
meta: [],
)

View File

@ -0,0 +1,11 @@
body-generic = creature
body-biped_large-ogre = ogre
body-biped_large-cyclops = cyclops
body-biped_large-wendigo = wendigo
body-biped_large-werewolf = werewolf
body-biped_large-cave_troll = cave troll
body-biped_large-mountain_troll = mountain troll
body-biped_large-swamp_troll = swamp troll
body-biped_large-blue_oni = blue oni
body-biped_large-red_oni = red oni
body-biped_large-tursus = tursus

View File

@ -253,7 +253,11 @@ npc-speech-merchant_sell_directed =
npc-speech-tell_site = npc-speech-tell_site =
.a0 = Have you visited { $site }? It's just { $dir } of here! .a0 = Have you visited { $site }? It's just { $dir } of here!
.a1 = You should visit { $site } some time. .a1 = You should visit { $site } some time.
.a2 = If you travel { $dir }, you can get to { $site }. .a2 = If you travel { $dist } to the { $dir }, you can get to { $site }.
.a3 = To the { $dir } you'll find { $site }, it's { $dist }.
npc-speech-tell_monster =
.a0 = They say there's a { $body } to the { $dir }, { $dist }...
.a1 = You think you're tough? To the { $dir } there's a { $body }.
npc-speech-witness_murder = npc-speech-witness_murder =
.a0 = Murderer! .a0 = Murderer!
.a1 = How could you do this? .a1 = How could you do this?
@ -274,3 +278,9 @@ npc-speech-dir_south = south
npc-speech-dir_south_west = south-west npc-speech-dir_south_west = south-west
npc-speech-dir_west = west npc-speech-dir_west = west
npc-speech-dir_north_west = north-west npc-speech-dir_north_west = north-west
npc-speech-dist_very_far = very far away
npc-speech-dist_far = far away
npc-speech-dist_ahead = some way away
npc-speech-dist_near = nearby
npc-speech-dist_near_to = very close

View File

@ -199,7 +199,7 @@ float lights_at(vec3 wpos, vec3 wnorm, vec3 /*cam_to_frag*/view_dir, vec3 mu, ve
float computed_shadow = ShadowCalculationPoint(i, -difference, wnorm, wpos/*, light_distance*/); float computed_shadow = ShadowCalculationPoint(i, -difference, wnorm, wpos/*, light_distance*/);
// directed_light += is_direct ? max(computed_shadow, /*LIGHT_AMBIANCE*/0.0) * direct_light : vec3(0.0); // directed_light += is_direct ? max(computed_shadow, /*LIGHT_AMBIANCE*/0.0) * direct_light : vec3(0.0);
// Non-physically emulate ambient light nearby // Non-physically emulate ambient light nearby
float ambiance = (dot(-wnorm, direct_light_dir) * 0.5 + 0.5) * strength; float ambiance = mix(0.05, 0.5, (dot(wnorm, direct_light_dir) + 1.0) * 0.5) * strength;
#ifdef FIGURE_SHADER #ifdef FIGURE_SHADER
// Non-physical hack. Subtle, but allows lanterns to glow nicely // Non-physical hack. Subtle, but allows lanterns to glow nicely
// TODO: Make lanterns use glowing cells instead // TODO: Make lanterns use glowing cells instead

View File

@ -23,6 +23,7 @@ use common::{
self, self,
chat::KillSource, chat::KillSource,
controller::CraftEvent, controller::CraftEvent,
dialogue::Subject,
group, group,
inventory::item::{modular, tool, ItemKind}, inventory::item::{modular, tool, ItemKind},
invite::{InviteKind, InviteResponse}, invite::{InviteKind, InviteResponse},
@ -1132,14 +1133,16 @@ impl Client {
} }
} }
pub fn npc_interact(&mut self, npc_entity: EcsEntity) { pub fn npc_interact(&mut self, npc_entity: EcsEntity, subject: Subject) {
// If we're dead, exit before sending message // If we're dead, exit before sending message
if self.is_dead() { if self.is_dead() {
return; return;
} }
if let Some(uid) = self.state.read_component_copied(npc_entity) { if let Some(uid) = self.state.read_component_copied(npc_entity) {
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::Interact(uid))); self.send_msg(ClientGeneral::ControlEvent(ControlEvent::Interact(
uid, subject,
)));
} }
} }

View File

@ -4,7 +4,7 @@ use crate::{
quadruped_small, ship, Body, UtteranceKind, quadruped_small, ship, Body, UtteranceKind,
}, },
path::Chaser, path::Chaser,
rtsim::RtSimController, rtsim::{NpcInput, RtSimController},
trade::{PendingTrade, ReducedInventory, SiteId, SitePrices, TradeId, TradeResult}, trade::{PendingTrade, ReducedInventory, SiteId, SitePrices, TradeId, TradeResult},
uid::Uid, uid::Uid,
}; };
@ -569,6 +569,8 @@ pub struct Agent {
/// required and reset each time the flee timer is reset. /// required and reset each time the flee timer is reset.
pub flee_from_pos: Option<Pos>, pub flee_from_pos: Option<Pos>,
pub awareness: Awareness, pub awareness: Awareness,
/// Inputs sent up to rtsim
pub rtsim_outbox: Option<VecDeque<NpcInput>>,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -673,6 +675,7 @@ impl Agent {
position_pid_controller: None, position_pid_controller: None,
flee_from_pos: None, flee_from_pos: None,
awareness: Awareness::new(0.0), awareness: Awareness::new(0.0),
rtsim_outbox: None,
} }
} }

View File

@ -18,6 +18,7 @@ pub mod theropod;
use crate::{ use crate::{
assets::{self, Asset}, assets::{self, Asset},
comp::Content,
consts::{HUMAN_DENSITY, WATER_DENSITY}, consts::{HUMAN_DENSITY, WATER_DENSITY},
make_case_elim, make_case_elim,
npc::NpcKind, npc::NpcKind,
@ -1076,6 +1077,13 @@ impl Body {
} }
.into() .into()
} }
pub fn localize(&self) -> Content {
match self {
Self::BipedLarge(biped_large) => biped_large.localize(),
_ => Content::localized("body-generic"),
}
}
} }
impl Component for Body { impl Component for Body {

View File

@ -1,4 +1,4 @@
use crate::{make_case_elim, make_proj_elim}; use crate::{comp::Content, make_case_elim, make_proj_elim};
use rand::{seq::SliceRandom, thread_rng}; use rand::{seq::SliceRandom, thread_rng};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -23,6 +23,22 @@ impl Body {
let body_type = *ALL_BODY_TYPES.choose(rng).unwrap(); let body_type = *ALL_BODY_TYPES.choose(rng).unwrap();
Self { species, body_type } Self { species, body_type }
} }
pub fn localize(&self) -> Content {
Content::localized(match &self.species {
Species::Ogre => "body-biped_large-ogre",
Species::Cyclops => "body-biped_large-cyclops",
Species::Wendigo => "body-biped_large-wendigo",
Species::Werewolf => "body-biped_large-werewolf",
Species::Cavetroll => "body-biped_large-cave_troll",
Species::Mountaintroll => "body-biped_large-mountain_troll",
Species::Swamptroll => "body-biped_large-swamp_troll",
Species::Blueoni => "body-biped_large-blue_oni",
Species::Redoni => "body-biped_large-red_oni",
Species::Tursus => "body-biped_large-tursus",
_ => "body-generic",
})
}
} }
impl From<Body> for super::Body { impl From<Body> for super::Body {

View File

@ -102,4 +102,14 @@ impl Distance {
Distance::NextTo => "just around", Distance::NextTo => "just around",
} }
} }
pub fn localize_npc(&self) -> Content {
Content::localized(match self {
Self::VeryFar => "npc-speech-dist_very_far",
Self::Far => "npc-speech-dist_far",
Self::Ahead => "npc-speech-dist_ahead",
Self::Near => "npc-speech-dist_near",
Self::NextTo => "npc-speech-dist_near_to",
})
}
} }

View File

@ -1,6 +1,7 @@
use crate::{ use crate::{
comp::{ comp::{
ability, ability,
dialogue::Subject,
inventory::{ inventory::{
item::tool::ToolKind, item::tool::ToolKind,
slot::{EquipSlot, InvSlotId, Slot}, slot::{EquipSlot, InvSlotId, Slot},
@ -137,7 +138,7 @@ pub enum ControlEvent {
//ToggleLantern, //ToggleLantern,
EnableLantern, EnableLantern,
DisableLantern, DisableLantern,
Interact(Uid), Interact(Uid, Subject),
InitiateInvite(Uid, InviteKind), InitiateInvite(Uid, InviteKind),
InviteResponse(InviteResponse), InviteResponse(InviteResponse),
PerformTradeAction(TradeId, TradeAction), PerformTradeAction(TradeId, TradeAction),

View File

@ -1,23 +1,24 @@
use serde::{Deserialize, Serialize};
use vek::{Vec2, Vec3}; use vek::{Vec2, Vec3};
use super::Item; use super::Item;
#[derive(Clone, Debug)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct AskedLocation { pub struct AskedLocation {
pub name: String, pub name: String,
pub origin: Vec2<i32>, pub origin: Vec2<i32>,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum PersonType { pub enum PersonType {
Merchant, Merchant,
Villager { name: String }, Villager { name: String },
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct AskedPerson { pub struct AskedPerson {
pub person_type: PersonType, pub person_type: PersonType,
pub origin: Option<Vec3<f32>>, pub origin: Option<Vec3<i32>>,
} }
impl AskedPerson { impl AskedPerson {
@ -30,7 +31,7 @@ impl AskedPerson {
} }
/// Conversation subject /// Conversation subject
#[derive(Clone, Debug)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Subject { pub enum Subject {
/// Using simple interaction with NPC /// Using simple interaction with NPC
/// This is meant to be the default behavior of talking /// This is meant to be the default behavior of talking

View File

@ -40,6 +40,14 @@ impl PresenceKind {
/// certain in-game messages from the client such as control inputs /// certain in-game messages from the client such as control inputs
/// should be handled. /// should be handled.
pub fn controlling_char(&self) -> bool { matches!(self, Self::Character(_) | Self::Possessor) } pub fn controlling_char(&self) -> bool { matches!(self, Self::Character(_) | Self::Possessor) }
pub fn character_id(&self) -> Option<CharacterId> {
if let Self::Character(character_id) = self {
Some(*character_id)
} else {
None
}
}
} }
#[derive(PartialEq, Debug, Clone, Copy)] #[derive(PartialEq, Debug, Clone, Copy)]

View File

@ -3,6 +3,7 @@ use crate::{
comp::{ comp::{
self, self,
agent::Sound, agent::Sound,
dialogue::Subject,
invite::{InviteKind, InviteResponse}, invite::{InviteKind, InviteResponse},
DisconnectReason, Ori, Pos, DisconnectReason, Ori, Pos,
}, },
@ -189,7 +190,7 @@ pub enum ServerEvent {
}, },
EnableLantern(EcsEntity), EnableLantern(EcsEntity),
DisableLantern(EcsEntity), DisableLantern(EcsEntity),
NpcInteract(EcsEntity, EcsEntity), NpcInteract(EcsEntity, EcsEntity, Subject),
InviteResponse(EcsEntity, InviteResponse), InviteResponse(EcsEntity, InviteResponse),
InitiateInvite(EcsEntity, Uid, InviteKind), InitiateInvite(EcsEntity, Uid, InviteKind),
ProcessTradeAction(EcsEntity, TradeId, TradeAction), ProcessTradeAction(EcsEntity, TradeId, TradeAction),

View File

@ -3,7 +3,10 @@
// `Agent`). When possible, this should be moved to the `rtsim` // `Agent`). When possible, this should be moved to the `rtsim`
// module in `server`. // module in `server`.
use crate::{character::CharacterId, comp::Content}; use crate::{
character::CharacterId,
comp::{dialogue::Subject, Content},
};
use rand::{seq::IteratorRandom, Rng}; use rand::{seq::IteratorRandom, Rng};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use specs::Component; use specs::Component;
@ -19,6 +22,8 @@ slotmap::new_key_type! { pub struct SiteId; }
slotmap::new_key_type! { pub struct FactionId; } slotmap::new_key_type! { pub struct FactionId; }
slotmap::new_key_type! { pub struct ReportId; }
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]
pub struct RtSimEntity(pub NpcId); pub struct RtSimEntity(pub NpcId);
@ -258,6 +263,13 @@ pub enum NpcAction {
Attack(Actor), Attack(Actor),
} }
// Represents a message passed back to rtsim from an agent's brain
#[derive(Clone, Debug)]
pub enum NpcInput {
Report(ReportId),
Interaction(Actor, Subject),
}
// Note: the `serde(name = "...")` is to minimise the length of field // Note: the `serde(name = "...")` is to minimise the length of field
// identifiers for the sake of rtsim persistence // identifiers for the sake of rtsim persistence
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, enum_map::Enum)] #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, enum_map::Enum)]
@ -286,6 +298,18 @@ pub enum ChunkResource {
Ore, // Iron, copper, etc. Ore, // Iron, copper, etc.
} }
// Note: the `serde(name = "...")` is to minimise the length of field
// identifiers for the sake of rtsim persistence
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum Role {
#[serde(rename = "0")]
Civilised(Option<Profession>),
#[serde(rename = "1")]
Wild,
#[serde(rename = "2")]
Monster,
}
// Note: the `serde(name = "...")` is to minimise the length of field // Note: the `serde(name = "...")` is to minimise the length of field
// identifiers for the sake of rtsim persistence // identifiers for the sake of rtsim persistence
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]

View File

@ -80,6 +80,7 @@ impl TerrainChunkSize {
pub trait CoordinateConversions { pub trait CoordinateConversions {
fn wpos_to_cpos(&self) -> Self; fn wpos_to_cpos(&self) -> Self;
fn cpos_to_wpos(&self) -> Self; fn cpos_to_wpos(&self) -> Self;
fn cpos_to_wpos_center(&self) -> Self;
} }
impl CoordinateConversions for Vec2<i32> { impl CoordinateConversions for Vec2<i32> {
@ -90,6 +91,13 @@ impl CoordinateConversions for Vec2<i32> {
#[inline] #[inline]
fn cpos_to_wpos(&self) -> Self { self.map2(TerrainChunkSize::RECT_SIZE, |e, sz| e * sz as i32) } fn cpos_to_wpos(&self) -> Self { self.map2(TerrainChunkSize::RECT_SIZE, |e, sz| e * sz as i32) }
#[inline]
fn cpos_to_wpos_center(&self) -> Self {
self.map2(TerrainChunkSize::RECT_SIZE, |e, sz| {
e * sz as i32 + sz as i32 / 2
})
}
} }
impl CoordinateConversions for Vec2<f32> { impl CoordinateConversions for Vec2<f32> {
@ -98,6 +106,13 @@ impl CoordinateConversions for Vec2<f32> {
#[inline] #[inline]
fn cpos_to_wpos(&self) -> Self { self.map2(TerrainChunkSize::RECT_SIZE, |e, sz| e * sz as f32) } fn cpos_to_wpos(&self) -> Self { self.map2(TerrainChunkSize::RECT_SIZE, |e, sz| e * sz as f32) }
#[inline]
fn cpos_to_wpos_center(&self) -> Self {
self.map2(TerrainChunkSize::RECT_SIZE, |e, sz| {
e * sz as f32 + sz as f32 / 2.0
})
}
} }
impl CoordinateConversions for Vec2<f64> { impl CoordinateConversions for Vec2<f64> {
@ -106,6 +121,13 @@ impl CoordinateConversions for Vec2<f64> {
#[inline] #[inline]
fn cpos_to_wpos(&self) -> Self { self.map2(TerrainChunkSize::RECT_SIZE, |e, sz| e * sz as f64) } fn cpos_to_wpos(&self) -> Self { self.map2(TerrainChunkSize::RECT_SIZE, |e, sz| e * sz as f64) }
#[inline]
fn cpos_to_wpos_center(&self) -> Self {
self.map2(TerrainChunkSize::RECT_SIZE, |e, sz| {
e * sz as f64 + sz as f64 / 2.0
})
}
} }
// TerrainChunkMeta // TerrainChunkMeta

View File

@ -66,12 +66,13 @@ impl<'a> System<'a> for Sys {
ControlEvent::DisableLantern => { ControlEvent::DisableLantern => {
server_emitter.emit(ServerEvent::DisableLantern(entity)) server_emitter.emit(ServerEvent::DisableLantern(entity))
}, },
ControlEvent::Interact(npc_uid) => { ControlEvent::Interact(npc_uid, subject) => {
if let Some(npc_entity) = read_data if let Some(npc_entity) = read_data
.uid_allocator .uid_allocator
.retrieve_entity_internal(npc_uid.id()) .retrieve_entity_internal(npc_uid.id())
{ {
server_emitter.emit(ServerEvent::NpcInteract(entity, npc_entity)); server_emitter
.emit(ServerEvent::NpcInteract(entity, npc_entity, subject));
} }
}, },
ControlEvent::InitiateInvite(inviter_uid, kind) => { ControlEvent::InitiateInvite(inviter_uid, kind) => {

View File

@ -5,8 +5,12 @@ use crate::{
}, },
RtState, RtState,
}; };
use common::resources::{Time, TimeOfDay}; use common::{
resources::{Time, TimeOfDay},
rtsim::NpcInput,
};
use hashbrown::HashSet; use hashbrown::HashSet;
use itertools::Either;
use rand_chacha::ChaChaRng; use rand_chacha::ChaChaRng;
use std::{any::Any, collections::VecDeque, marker::PhantomData, ops::ControlFlow}; use std::{any::Any, collections::VecDeque, marker::PhantomData, ops::ControlFlow};
use world::{IndexRef, World}; use world::{IndexRef, World};
@ -25,7 +29,7 @@ pub struct NpcCtx<'a> {
pub npc_id: NpcId, pub npc_id: NpcId,
pub npc: &'a Npc, pub npc: &'a Npc,
pub controller: &'a mut Controller, pub controller: &'a mut Controller,
pub inbox: &'a mut VecDeque<ReportId>, // TODO: Allow more inbox items pub inbox: &'a mut VecDeque<NpcInput>, // TODO: Allow more inbox items
pub sentiments: &'a mut Sentiments, pub sentiments: &'a mut Sentiments,
pub known_reports: &'a mut HashSet<ReportId>, pub known_reports: &'a mut HashSet<ReportId>,
@ -227,6 +231,22 @@ pub trait Action<R = ()>: Any + Send + Sync {
{ {
Debug(self, mk_info, PhantomData) Debug(self, mk_info, PhantomData)
} }
#[must_use]
fn l<Rhs>(self) -> Either<Self, Rhs>
where
Self: Sized,
{
Either::Left(self)
}
#[must_use]
fn r<Lhs>(self) -> Either<Lhs, Self>
where
Self: Sized,
{
Either::Right(self)
}
} }
impl<R: 'static> Action<R> for Box<dyn Action<R>> { impl<R: 'static> Action<R> for Box<dyn Action<R>> {
@ -246,11 +266,11 @@ impl<R: 'static> Action<R> for Box<dyn Action<R>> {
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<R> { (**self).tick(ctx) } fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<R> { (**self).tick(ctx) }
} }
impl<R: 'static, A: Action<R>, B: Action<R>> Action<R> for itertools::Either<A, B> { impl<R: 'static, A: Action<R>, B: Action<R>> Action<R> for Either<A, B> {
fn is_same(&self, other: &Self) -> bool { fn is_same(&self, other: &Self) -> bool {
match (self, other) { match (self, other) {
(itertools::Either::Left(x), itertools::Either::Left(y)) => x.is_same(y), (Either::Left(x), Either::Left(y)) => x.is_same(y),
(itertools::Either::Right(x), itertools::Either::Right(y)) => x.is_same(y), (Either::Right(x), Either::Right(y)) => x.is_same(y),
_ => false, _ => false,
} }
} }
@ -259,22 +279,22 @@ impl<R: 'static, A: Action<R>, B: Action<R>> Action<R> for itertools::Either<A,
fn backtrace(&self, bt: &mut Vec<String>) { fn backtrace(&self, bt: &mut Vec<String>) {
match self { match self {
itertools::Either::Left(x) => x.backtrace(bt), Either::Left(x) => x.backtrace(bt),
itertools::Either::Right(x) => x.backtrace(bt), Either::Right(x) => x.backtrace(bt),
} }
} }
fn reset(&mut self) { fn reset(&mut self) {
match self { match self {
itertools::Either::Left(x) => x.reset(), Either::Left(x) => x.reset(),
itertools::Either::Right(x) => x.reset(), Either::Right(x) => x.reset(),
} }
} }
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<R> { fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<R> {
match self { match self {
itertools::Either::Left(x) => x.tick(ctx), Either::Left(x) => x.tick(ctx),
itertools::Either::Right(x) => x.tick(ctx), Either::Right(x) => x.tick(ctx),
} }
} }
} }

View File

@ -1,6 +1,6 @@
use crate::{ use crate::{
ai::Action, ai::Action,
data::{ReportId, Reports, Sentiments}, data::{Reports, Sentiments},
gen::name, gen::name,
}; };
pub use common::rtsim::{NpcId, Profession}; pub use common::rtsim::{NpcId, Profession};
@ -9,7 +9,8 @@ use common::{
comp, comp,
grid::Grid, grid::Grid,
rtsim::{ rtsim::{
Actor, ChunkResource, FactionId, NpcAction, NpcActivity, Personality, SiteId, VehicleId, Actor, ChunkResource, FactionId, NpcAction, NpcActivity, NpcInput, Personality, ReportId,
Role, SiteId, VehicleId,
}, },
store::Id, store::Id,
terrain::CoordinateConversions, terrain::CoordinateConversions,
@ -96,7 +97,7 @@ pub struct Npc {
pub wpos: Vec3<f32>, pub wpos: Vec3<f32>,
pub body: comp::Body, pub body: comp::Body,
pub profession: Option<Profession>, pub role: Role,
pub home: Option<SiteId>, pub home: Option<SiteId>,
pub faction: Option<FactionId>, pub faction: Option<FactionId>,
pub riding: Option<Riding>, pub riding: Option<Riding>,
@ -120,7 +121,7 @@ pub struct Npc {
#[serde(skip)] #[serde(skip)]
pub controller: Controller, pub controller: Controller,
#[serde(skip)] #[serde(skip)]
pub inbox: VecDeque<ReportId>, pub inbox: VecDeque<NpcInput>,
/// Whether the NPC is in simulated or loaded mode (when rtsim is run on the /// Whether the NPC is in simulated or loaded mode (when rtsim is run on the
/// server, loaded corresponds to being within a loaded chunk). When in /// server, loaded corresponds to being within a loaded chunk). When in
@ -138,7 +139,7 @@ impl Clone for Npc {
Self { Self {
seed: self.seed, seed: self.seed,
wpos: self.wpos, wpos: self.wpos,
profession: self.profession.clone(), role: self.role.clone(),
home: self.home, home: self.home,
faction: self.faction, faction: self.faction,
riding: self.riding.clone(), riding: self.riding.clone(),
@ -162,14 +163,14 @@ impl Npc {
pub const PERM_ENTITY_CONFIG: u32 = 1; pub const PERM_ENTITY_CONFIG: u32 = 1;
const PERM_NAME: u32 = 0; const PERM_NAME: u32 = 0;
pub fn new(seed: u32, wpos: Vec3<f32>, body: comp::Body) -> Self { pub fn new(seed: u32, wpos: Vec3<f32>, body: comp::Body, role: Role) -> Self {
Self { Self {
seed, seed,
wpos, wpos,
body, body,
personality: Default::default(), personality: Default::default(),
sentiments: Default::default(), sentiments: Default::default(),
profession: None, role,
home: None, home: None,
faction: None, faction: None,
riding: None, riding: None,
@ -190,11 +191,15 @@ impl Npc {
self self
} }
// TODO: have a dedicated `NpcBuilder` type for this. // // TODO: have a dedicated `NpcBuilder` type for this.
pub fn with_profession(mut self, profession: impl Into<Option<Profession>>) -> Self { // pub fn with_profession(mut self, profession: impl Into<Option<Profession>>)
self.profession = profession.into(); // -> Self { if let Role::Humanoid(p) = &mut self.role {
self // *p = profession.into();
} // } else {
// panic!("Tried to assign profession {:?} to NPC, but has role {:?},
// which cannot have a profession", profession.into(), self.role); }
// self
// }
// TODO: have a dedicated `NpcBuilder` type for this. // TODO: have a dedicated `NpcBuilder` type for this.
pub fn with_home(mut self, home: impl Into<Option<SiteId>>) -> Self { pub fn with_home(mut self, home: impl Into<Option<SiteId>>) -> Self {
@ -232,6 +237,13 @@ impl Npc {
// once we've decided that we want to // once we've decided that we want to
pub fn get_name(&self) -> String { name::generate(&mut self.rng(Self::PERM_NAME)) } pub fn get_name(&self) -> String { name::generate(&mut self.rng(Self::PERM_NAME)) }
pub fn profession(&self) -> Option<Profession> {
match &self.role {
Role::Civilised(profession) => profession.clone(),
Role::Monster | Role::Wild => None,
}
}
pub fn cleanup(&mut self, reports: &Reports) { pub fn cleanup(&mut self, reports: &Reports) {
// Clear old or superfluous sentiments // Clear old or superfluous sentiments
// TODO: It might be worth giving more important NPCs a higher sentiment // TODO: It might be worth giving more important NPCs a higher sentiment

View File

@ -4,7 +4,7 @@ use slotmap::HopSlotMap;
use std::ops::Deref; use std::ops::Deref;
use vek::*; use vek::*;
slotmap::new_key_type! { pub struct ReportId; } pub use common::rtsim::ReportId;
/// Represents a single piece of information known by an rtsim entity. /// Represents a single piece of information known by an rtsim entity.
/// ///

View File

@ -12,14 +12,14 @@ use common::{
comp::{self, Body}, comp::{self, Body},
grid::Grid, grid::Grid,
resources::TimeOfDay, resources::TimeOfDay,
rtsim::{Personality, WorldSettings}, rtsim::{Personality, Role, WorldSettings},
terrain::TerrainChunkSize, terrain::{BiomeKind, CoordinateConversions, TerrainChunkSize},
vol::RectVolSize, vol::RectVolSize,
}; };
use rand::prelude::*; use rand::prelude::*;
use tracing::info; use tracing::info;
use vek::*; use vek::*;
use world::{site::SiteKind, site2::PlotKind, IndexRef, World}; use world::{site::SiteKind, site2::PlotKind, IndexRef, World, CONFIG};
impl Data { impl Data {
pub fn generate(settings: &WorldSettings, world: &World, index: IndexRef) -> Self { pub fn generate(settings: &WorldSettings, world: &World, index: IndexRef) -> Self {
@ -124,20 +124,20 @@ impl Data {
rng.gen(), rng.gen(),
rand_wpos(&mut rng, matches_buildings), rand_wpos(&mut rng, matches_buildings),
random_humanoid(&mut rng), random_humanoid(&mut rng),
Role::Civilised(Some(match rng.gen_range(0..20) {
0 => Profession::Hunter,
1 => Profession::Blacksmith,
2 => Profession::Chef,
3 => Profession::Alchemist,
5..=8 => Profession::Farmer,
9..=10 => Profession::Herbalist,
11..=16 => Profession::Guard,
_ => Profession::Adventurer(rng.gen_range(0..=3)),
})),
) )
.with_faction(site.faction) .with_faction(site.faction)
.with_home(site_id) .with_home(site_id)
.with_personality(Personality::random(&mut rng)) .with_personality(Personality::random(&mut rng)),
.with_profession(match rng.gen_range(0..20) {
0 => Profession::Hunter,
1 => Profession::Blacksmith,
2 => Profession::Chef,
3 => Profession::Alchemist,
5..=8 => Profession::Farmer,
9..=10 => Profession::Herbalist,
11..=16 => Profession::Guard,
_ => Profession::Adventurer(rng.gen_range(0..=3)),
}),
); );
} }
} else { } else {
@ -147,11 +147,11 @@ impl Data {
rng.gen(), rng.gen(),
rand_wpos(&mut rng, matches_buildings), rand_wpos(&mut rng, matches_buildings),
random_humanoid(&mut rng), random_humanoid(&mut rng),
Role::Civilised(Some(Profession::Cultist)),
) )
.with_personality(Personality::random_evil(&mut rng)) .with_personality(Personality::random_evil(&mut rng))
.with_faction(site.faction) .with_faction(site.faction)
.with_home(site_id) .with_home(site_id),
.with_profession(Profession::Cultist),
); );
} }
} }
@ -163,10 +163,10 @@ impl Data {
rng.gen(), rng.gen(),
rand_wpos(&mut rng, matches_plazas), rand_wpos(&mut rng, matches_plazas),
random_humanoid(&mut rng), random_humanoid(&mut rng),
Role::Civilised(Some(Profession::Merchant)),
) )
.with_home(site_id) .with_home(site_id)
.with_personality(Personality::random_good(&mut rng)) .with_personality(Personality::random_good(&mut rng)),
.with_profession(Profession::Merchant),
); );
} }
} }
@ -178,11 +178,15 @@ impl Data {
.create_vehicle(Vehicle::new(wpos, comp::body::ship::Body::DefaultAirship)); .create_vehicle(Vehicle::new(wpos, comp::body::ship::Body::DefaultAirship));
this.npcs.create_npc( this.npcs.create_npc(
Npc::new(rng.gen(), wpos, random_humanoid(&mut rng)) Npc::new(
.with_home(site_id) rng.gen(),
.with_profession(Profession::Captain) wpos,
.with_personality(Personality::random_good(&mut rng)) random_humanoid(&mut rng),
.steering(vehicle_id), Role::Civilised(Some(Profession::Captain)),
)
.with_home(site_id)
.with_personality(Personality::random_good(&mut rng))
.steering(vehicle_id),
); );
} }
} }
@ -211,10 +215,56 @@ impl Data {
rng.gen(), rng.gen(),
rand_wpos(&mut rng), rand_wpos(&mut rng),
Body::BirdLarge(comp::body::bird_large::Body::random_with(&mut rng, species)), Body::BirdLarge(comp::body::bird_large::Body::random_with(&mut rng, species)),
Role::Wild,
) )
.with_home(site_id), .with_home(site_id),
); );
} }
// Spawn monsters into the world
for _ in 0..100 {
// Try a few times to find a location that's not underwater
if let Some((wpos, chunk)) = (0..10)
.map(|_| world.sim().get_size().map(|sz| rng.gen_range(0..sz as i32)))
.find_map(|pos| Some((pos, world.sim().get(pos).filter(|c| !c.is_underwater())?)))
.map(|(pos, chunk)| {
let wpos2d = pos.cpos_to_wpos_center();
(
wpos2d
.map(|e| e as f32 + 0.5)
.with_z(world.sim().get_alt_approx(wpos2d).unwrap_or(0.0)),
chunk,
)
})
{
let biome = chunk.get_biome();
let Some(species) = [
Some(comp::body::biped_large::Species::Ogre),
Some(comp::body::biped_large::Species::Cyclops),
Some(comp::body::biped_large::Species::Wendigo).filter(|_| biome == BiomeKind::Taiga),
Some(comp::body::biped_large::Species::Cavetroll),
Some(comp::body::biped_large::Species::Mountaintroll).filter(|_| biome == BiomeKind::Mountain),
Some(comp::body::biped_large::Species::Swamptroll).filter(|_| biome == BiomeKind::Swamp),
Some(comp::body::biped_large::Species::Blueoni),
Some(comp::body::biped_large::Species::Redoni),
Some(comp::body::biped_large::Species::Tursus).filter(|_| chunk.temp < CONFIG.snow_temp),
]
.into_iter()
.flatten()
.choose(&mut rng)
else { continue };
this.npcs.create_npc(Npc::new(
rng.gen(),
wpos,
Body::BipedLarge(comp::body::biped_large::Body::random_with(
&mut rng, &species,
)),
Role::Monster,
));
}
}
info!("Generated {} rtsim NPCs.", this.npcs.len()); info!("Generated {} rtsim NPCs.", this.npcs.len());
this this

View File

@ -11,9 +11,13 @@ use crate::{
}; };
use common::{ use common::{
astar::{Astar, PathResult}, astar::{Astar, PathResult},
comp::{compass::Direction, Content}, comp::{
compass::{Direction, Distance},
dialogue::Subject,
Content,
},
path::Path, path::Path,
rtsim::{Actor, ChunkResource, Profession, SiteId}, rtsim::{Actor, ChunkResource, NpcInput, Profession, Role, SiteId},
spiral::Spiral2d, spiral::Spiral2d,
store::Id, store::Id,
terrain::{CoordinateConversions, SiteKindMeta, TerrainChunkSize}, terrain::{CoordinateConversions, SiteKindMeta, TerrainChunkSize},
@ -547,19 +551,72 @@ fn timeout(time: f64) -> impl FnMut(&mut NpcCtx) -> bool + Clone + Send + Sync {
move |ctx| ctx.time.0 > *timeout.get_or_insert(ctx.time.0 + time) move |ctx| ctx.time.0 > *timeout.get_or_insert(ctx.time.0 + time)
} }
fn talk_to(tgt: Actor, _subject: Option<Subject>) -> impl Action {
now(move |ctx| {
if matches!(tgt, Actor::Npc(_)) && ctx.rng.gen_bool(0.2) {
// Cut off the conversation sometimes to avoid infinite conversations (but only
// if the target is an NPC!) TODO: Don't special case this, have
// some sort of 'bored of conversation' system
idle().l()
} else {
// Mention nearby sites
let comment = 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()),
("dist", Distance::from_length(mention_site.wpos.as_().distance(ctx.npc.wpos.xy()) as i32).localize_npc()),
])
// Mention nearby monsters
} else if ctx.rng.gen_bool(0.3)
&& let Some(monster) = ctx.state.data().npcs
.values()
.filter(|other| matches!(&other.role, Role::Monster))
.min_by_key(|other| other.wpos.xy().distance(ctx.npc.wpos.xy()) as i32)
{
Content::localized_with_args("npc-speech-tell_monster", [
("body", monster.body.localize()),
("dir", Direction::from_dir(monster.wpos.xy() - ctx.npc.wpos.xy()).localize_npc()),
("dist", Distance::from_length(monster.wpos.xy().distance(ctx.npc.wpos.xy()) as i32).localize_npc()),
])
} else {
ctx.npc.personality.get_generic_comment(&mut ctx.rng)
};
// TODO: Don't special-case players
let wait = if matches!(tgt, Actor::Character(_)) {
0.0
} else {
1.5
};
idle()
.repeat()
.stop_if(timeout(wait))
.then(just(move |ctx| ctx.controller.say(tgt, comment.clone())))
.r()
}
})
}
fn socialize() -> impl Action { fn socialize() -> impl Action {
now(|ctx| { now(|ctx| {
// Skip most socialising actions if we're not loaded // Skip most socialising actions if we're not loaded
if matches!(ctx.npc.mode, SimulationMode::Loaded) && ctx.rng.gen_bool(0.002) { if matches!(ctx.npc.mode, SimulationMode::Loaded) && ctx.rng.gen_bool(0.002) {
// Sometimes dance
if ctx.rng.gen_bool(0.15) { if ctx.rng.gen_bool(0.15) {
return Either::Left( return just(|ctx| ctx.controller.do_dance())
just(|ctx| ctx.controller.do_dance()) .repeat()
.repeat() .stop_if(timeout(6.0))
.stop_if(timeout(6.0)) .debug(|| "dancing")
.debug(|| "dancing") .map(|_| ())
.map(|_| ()) .l()
.boxed(), .l();
); // Talk to nearby NPCs
} else if let Some(other) = ctx } else if let Some(other) = ctx
.state .state
.data() .data()
@ -567,31 +624,15 @@ fn socialize() -> impl Action {
.nearby(Some(ctx.npc_id), ctx.npc.wpos, 8.0) .nearby(Some(ctx.npc_id), ctx.npc.wpos, 8.0)
.choose(&mut ctx.rng) .choose(&mut ctx.rng)
{ {
return Either::Left( return talk_to(other, None)
just(move |ctx| ctx.controller.say(other, if ctx.rng.gen_bool(0.3) // After talking, wait for a while
&& 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
.then(idle().repeat().stop_if(timeout(4.0))) .then(idle().repeat().stop_if(timeout(4.0)))
.map(|_| ()) .map(|_| ())
.boxed(), .r().l();
);
} }
} }
Either::Right(idle()) idle().r()
}) })
} }
@ -619,7 +660,7 @@ fn adventure() -> impl Action {
.min_by_key(|(_, site)| site.wpos.as_().distance(ctx.npc.wpos.xy()) as i32) .min_by_key(|(_, site)| site.wpos.as_().distance(ctx.npc.wpos.xy()) as i32)
.map(|(site_id, _)| site_id) .map(|(site_id, _)| site_id)
{ {
let wait_time = if matches!(ctx.npc.profession, Some(Profession::Merchant)) { let wait_time = if matches!(ctx.npc.profession(), Some(Profession::Merchant)) {
60.0 * 15.0 60.0 * 15.0
} else { } else {
60.0 * 3.0 60.0 * 3.0
@ -737,7 +778,7 @@ fn villager(visiting_site: SiteId) -> impl Action {
} }
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))
{ {
return important( return important(
now(move |ctx| { now(move |ctx| {
@ -777,7 +818,7 @@ fn villager(visiting_site: SiteId) -> impl Action {
.debug(|| "find somewhere to sleep"), .debug(|| "find somewhere to sleep"),
); );
// Villagers with roles should perform those roles // Villagers with roles should perform those roles
} else if matches!(ctx.npc.profession, Some(Profession::Herbalist)) && ctx.rng.gen_bool(0.8) } else if matches!(ctx.npc.profession(), Some(Profession::Herbalist)) && ctx.rng.gen_bool(0.8)
{ {
if let Some(forest_wpos) = find_forest(ctx) { if let Some(forest_wpos) = find_forest(ctx) {
return casual( return casual(
@ -790,7 +831,7 @@ fn villager(visiting_site: SiteId) -> impl Action {
.map(|_| ()), .map(|_| ()),
); );
} }
} else if matches!(ctx.npc.profession, Some(Profession::Hunter)) && ctx.rng.gen_bool(0.8) { } else if matches!(ctx.npc.profession(), Some(Profession::Hunter)) && ctx.rng.gen_bool(0.8) {
if let Some(forest_wpos) = find_forest(ctx) { if let Some(forest_wpos) = find_forest(ctx) {
return casual( return casual(
just(|ctx| { just(|ctx| {
@ -806,7 +847,7 @@ fn villager(visiting_site: SiteId) -> impl Action {
.map(|_| ()), .map(|_| ()),
); );
} }
} else if matches!(ctx.npc.profession, Some(Profession::Guard)) && ctx.rng.gen_bool(0.7) { } 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 casual( return casual(
travel_to_point(plaza_wpos, 0.4) travel_to_point(plaza_wpos, 0.4)
@ -824,7 +865,7 @@ fn villager(visiting_site: SiteId) -> impl Action {
.map(|_| ()), .map(|_| ()),
); );
} }
} else if matches!(ctx.npc.profession, Some(Profession::Merchant)) && ctx.rng.gen_bool(0.8) } else if matches!(ctx.npc.profession(), Some(Profession::Merchant)) && ctx.rng.gen_bool(0.8)
{ {
return casual( return casual(
just(|ctx| { just(|ctx| {
@ -963,10 +1004,12 @@ fn captain() -> impl Action {
fn check_inbox(ctx: &mut NpcCtx) -> Option<impl Action> { fn check_inbox(ctx: &mut NpcCtx) -> Option<impl Action> {
loop { loop {
match ctx.inbox.pop_front() { match ctx.inbox.pop_front() {
Some(report_id) if !ctx.known_reports.contains(&report_id) => { Some(NpcInput::Report(report_id)) if !ctx.known_reports.contains(&report_id) => {
#[allow(clippy::single_match)] #[allow(clippy::single_match)]
match ctx.state.data().reports.get(report_id).map(|r| r.kind) { match ctx.state.data().reports.get(report_id).map(|r| r.kind) {
Some(ReportKind::Death { killer, actor, .. }) => { Some(ReportKind::Death { killer, actor, .. })
if matches!(&ctx.npc.role, Role::Civilised(_)) =>
{
// 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 // TODO: For now, we don't make sentiment changes if the killer was an
@ -1002,14 +1045,17 @@ fn check_inbox(ctx: &mut NpcCtx) -> Option<impl Action> {
"npc-speech-witness_death" "npc-speech-witness_death"
}; };
ctx.known_reports.insert(report_id); ctx.known_reports.insert(report_id);
break Some(just(move |ctx| { break Some(
ctx.controller.say(killer, Content::localized(phrase)) just(move |ctx| ctx.controller.say(killer, Content::localized(phrase)))
})); .l(),
);
}, },
None => {}, // Stale report, ignore Some(ReportKind::Death { .. }) => {}, // We don't care about death
None => {}, // Stale report, ignore
} }
}, },
Some(_) => {}, // Reports we already know of are ignored Some(NpcInput::Report(_)) => {}, // Reports we already know of are ignored
Some(NpcInput::Interaction(by, subject)) => break Some(talk_to(by, Some(subject)).r()),
None => break None, None => break None,
} }
} }
@ -1057,14 +1103,14 @@ fn humanoid() -> impl Action {
} }
} else { } else {
let action = if matches!( let action = if matches!(
ctx.npc.profession, ctx.npc.profession(),
Some(Profession::Adventurer(_) | Profession::Merchant) Some(Profession::Adventurer(_) | Profession::Merchant)
) { ) {
adventure().boxed() adventure().l().l()
} else if let Some(home) = ctx.npc.home { } else if let Some(home) = ctx.npc.home {
villager(home).boxed() villager(home).r().l()
} else { } else {
idle().boxed() // Homeless idle().r() // Homeless
}; };
casual(action.interrupt_with(react_to_events)) casual(action.interrupt_with(react_to_events))
@ -1114,10 +1160,28 @@ fn bird_large() -> impl Action {
}) })
} }
fn monster() -> impl Action {
let mut bearing = Vec2::zero();
now(move |ctx| {
bearing = bearing
.map(|e| e + ctx.rng.gen_range(-0.1..0.1))
.try_normalized()
.unwrap_or_default();
goto_2d(ctx.npc.wpos.xy() + bearing * 24.0, 0.7, 8.0)
.debug(move || format!("Moving with a bearing of {:?}", bearing))
})
.repeat()
.map(|_| ())
}
fn think() -> impl Action { fn think() -> impl Action {
choose(|ctx| match ctx.npc.body { now(|ctx| match ctx.npc.body {
common::comp::Body::Humanoid(_) => casual(humanoid()), common::comp::Body::Humanoid(_) => humanoid().l().l().l(),
common::comp::Body::BirdLarge(_) => casual(bird_large()), common::comp::Body::BirdLarge(_) => bird_large().r().l().l(),
_ => casual(socialize()), _ => match &ctx.npc.role {
Role::Civilised(_) => socialize().l().r().l(),
Role::Monster => monster().r().r().l(),
Role::Wild => idle().r(),
},
}) })
} }

View File

@ -3,6 +3,7 @@ use crate::{
event::{EventCtx, OnDeath}, event::{EventCtx, OnDeath},
RtState, Rule, RuleError, RtState, Rule, RuleError,
}; };
use common::rtsim::NpcInput;
pub struct ReportEvents; pub struct ReportEvents;
@ -38,7 +39,7 @@ fn on_death(ctx: EventCtx<ReportEvents, OnDeath>) {
// data structure in their own time. // data structure in their own time.
for npc_id in nearby { for npc_id in nearby {
if let Some(npc) = data.npcs.get_mut(npc_id) { if let Some(npc) = data.npcs.get_mut(npc_id) {
npc.inbox.push_back(report); npc.inbox.push_back(NpcInput::Report(report));
} }
} }
} }

View File

@ -82,67 +82,72 @@ fn on_death(ctx: EventCtx<SimulateNpcs, OnDeath>) {
Body::Humanoid(comp::humanoid::Body::random_with(rng, species)) Body::Humanoid(comp::humanoid::Body::random_with(rng, species))
}; };
let npc_id = data.spawn_npc( let npc_id = data.spawn_npc(
Npc::new(rng.gen(), rand_wpos(&mut rng), random_humanoid(&mut rng)) Npc::new(
.with_personality(Personality::random(&mut rng)) rng.gen(),
.with_home(site_id) rand_wpos(&mut rng),
.with_faction(npc.faction) random_humanoid(&mut rng),
.with_profession(npc.profession.clone()), npc.role.clone(),
)
.with_personality(Personality::random(&mut rng))
.with_home(site_id)
.with_faction(npc.faction),
); );
Some((npc_id, site_id)) Some((npc_id, Some(site_id)))
} else { } else {
warn!("No site found for respawning humanoid"); warn!("No site found for respawning humanoid");
None None
} }
}, },
Body::BirdLarge(_) => {
if let Some((site_id, site)) = data
.sites
.iter()
.filter(|(id, site)| {
Some(*id) != npc.home
&& site.world_site.map_or(false, |s| {
matches!(ctx.index.sites.get(s).kind, SiteKind::Dungeon(_))
})
})
.min_by_key(|(_, site)| site.population.len())
{
let rand_wpos = |rng: &mut ChaChaRng| {
let wpos2d = site.wpos.map(|e| e + rng.gen_range(-10..10));
wpos2d
.map(|e| e as f32 + 0.5)
.with_z(ctx.world.sim().get_alt_approx(wpos2d).unwrap_or(0.0))
};
let species = [
comp::body::bird_large::Species::Phoenix,
comp::body::bird_large::Species::Cockatrice,
comp::body::bird_large::Species::Roc,
]
.choose(&mut rng)
.unwrap();
let npc_id = data.npcs.create_npc(
Npc::new(
rng.gen(),
rand_wpos(&mut rng),
Body::BirdLarge(comp::body::bird_large::Body::random_with(
&mut rng, species,
)),
)
.with_home(site_id),
);
Some((npc_id, site_id))
} else {
warn!("No site found for respawning bird");
None
}
},
body => { body => {
error!("Tried to respawn rtsim NPC with invalid body: {:?}", body); let home = npc.home.and_then(|_| {
None data.sites
.iter()
.filter(|(id, site)| {
Some(*id) != npc.home
&& site.world_site.map_or(false, |s| {
matches!(ctx.index.sites.get(s).kind, SiteKind::Dungeon(_))
})
})
.min_by_key(|(_, site)| site.population.len())
});
let wpos = if let Some((_, home)) = home {
let wpos2d = home.wpos.map(|e| e + rng.gen_range(-10..10));
wpos2d
.map(|e| e as f32 + 0.5)
.with_z(ctx.world.sim().get_alt_approx(wpos2d).unwrap_or(0.0))
} else {
let pos = (0..10)
.map(|_| {
ctx.world
.sim()
.get_size()
.map(|sz| rng.gen_range(0..sz as i32))
})
.find(|pos| {
ctx.world
.sim()
.get(*pos)
.map_or(false, |c| !c.is_underwater())
})
.unwrap_or(ctx.world.sim().get_size().as_() / 2);
let wpos2d = pos.cpos_to_wpos_center();
wpos2d
.map(|e| e as f32 + 0.5)
.with_z(ctx.world.sim().get_alt_approx(wpos2d).unwrap_or(0.0))
};
let home = home.map(|(site_id, _)| site_id);
let npc_id = data.npcs.create_npc(
Npc::new(rng.gen(), wpos, body, npc.role.clone()).with_home(home),
);
Some((npc_id, home))
}, },
}; };
// Add the NPC to their home site // Add the NPC to their home site
if let Some((npc_id, home_site)) = details { if let Some((npc_id, Some(home_site))) = details {
if let Some(home) = data.sites.get_mut(home_site) { if let Some(home) = data.sites.get_mut(home_site) {
home.population.insert(npc_id); home.population.insert(npc_id);
} }

View File

@ -2,7 +2,11 @@ use crate::{
event::{EventCtx, OnDeath, OnSetup, OnTick}, event::{EventCtx, OnDeath, OnSetup, OnTick},
RtState, Rule, RuleError, RtState, Rule, RuleError,
}; };
use common::{grid::Grid, rtsim::Actor, terrain::CoordinateConversions}; use common::{
grid::Grid,
rtsim::{Actor, NpcInput},
terrain::CoordinateConversions,
};
pub struct SyncNpcs; pub struct SyncNpcs;
@ -124,7 +128,8 @@ fn on_tick(ctx: EventCtx<SyncNpcs, OnTick>) {
npc.inbox.extend(site.known_reports npc.inbox.extend(site.known_reports
.iter() .iter()
.copied() .copied()
.filter(|report| !npc.known_reports.contains(report))); .filter(|report| !npc.known_reports.contains(report))
.map(NpcInput::Report));
} }
} }

View File

@ -45,7 +45,7 @@ use common::{
outcome::Outcome, outcome::Outcome,
parse_cmd_args, parse_cmd_args,
resources::{BattleMode, PlayerPhysicsSettings, Secs, Time, TimeOfDay}, resources::{BattleMode, PlayerPhysicsSettings, Secs, Time, TimeOfDay},
rtsim::Actor, rtsim::{Actor, Role},
terrain::{Block, BlockKind, CoordinateConversions, SpriteKind, TerrainChunkSize}, terrain::{Block, BlockKind, CoordinateConversions, SpriteKind, TerrainChunkSize},
uid::{Uid, UidAllocator}, uid::{Uid, UidAllocator},
vol::ReadVol, vol::ReadVol,
@ -1269,7 +1269,7 @@ fn handle_rtsim_info(
let _ = writeln!(&mut info, "-- General Information --"); let _ = writeln!(&mut info, "-- General Information --");
let _ = writeln!(&mut info, "Seed: {}", npc.seed); let _ = writeln!(&mut info, "Seed: {}", npc.seed);
let _ = writeln!(&mut info, "Profession: {:?}", npc.profession); let _ = writeln!(&mut info, "Role: {:?}", npc.role);
let _ = writeln!(&mut info, "Home: {:?}", npc.home); let _ = writeln!(&mut info, "Home: {:?}", npc.home);
let _ = writeln!(&mut info, "Faction: {:?}", npc.faction); let _ = writeln!(&mut info, "Faction: {:?}", npc.faction);
let _ = writeln!(&mut info, "Personality: {:?}", npc.personality); let _ = writeln!(&mut info, "Personality: {:?}", npc.personality);
@ -1321,10 +1321,14 @@ fn handle_rtsim_npc(
.enumerate() .enumerate()
.filter(|(idx, npc)| { .filter(|(idx, npc)| {
let tags = [ let tags = [
npc.profession npc.profession()
.as_ref()
.map(|p| format!("{:?}", p)) .map(|p| format!("{:?}", p))
.unwrap_or_default(), .unwrap_or_default(),
match &npc.role {
Role::Civilised(_) => "civilised".to_string(),
Role::Wild => "wild".to_string(),
Role::Monster => "monster".to_string(),
},
format!("{:?}", npc.mode), format!("{:?}", npc.mode),
format!("{}", idx), format!("{}", idx),
]; ];

View File

@ -78,7 +78,12 @@ pub fn handle_lantern(server: &mut Server, entity: EcsEntity, enable: bool) {
} }
} }
pub fn handle_npc_interaction(server: &mut Server, interactor: EcsEntity, npc_entity: EcsEntity) { pub fn handle_npc_interaction(
server: &mut Server,
interactor: EcsEntity,
npc_entity: EcsEntity,
subject: Subject,
) {
let state = server.state_mut(); let state = server.state_mut();
if let Some(agent) = state if let Some(agent) = state
.ecs() .ecs()
@ -89,7 +94,7 @@ pub fn handle_npc_interaction(server: &mut Server, interactor: EcsEntity, npc_en
if let Some(interactor_uid) = state.ecs().uid_from_entity(interactor) { if let Some(interactor_uid) = state.ecs().uid_from_entity(interactor) {
agent agent
.inbox .inbox
.push_back(AgentEvent::Talk(interactor_uid, Subject::Regular)); .push_back(AgentEvent::Talk(interactor_uid, subject));
} }
} }
} }

View File

@ -123,8 +123,8 @@ impl Server {
}, },
ServerEvent::EnableLantern(entity) => handle_lantern(self, entity, true), ServerEvent::EnableLantern(entity) => handle_lantern(self, entity, true),
ServerEvent::DisableLantern(entity) => handle_lantern(self, entity, false), ServerEvent::DisableLantern(entity) => handle_lantern(self, entity, false),
ServerEvent::NpcInteract(interactor, target) => { ServerEvent::NpcInteract(interactor, target, subject) => {
handle_npc_interaction(self, interactor, target) handle_npc_interaction(self, interactor, target, subject)
}, },
ServerEvent::InitiateInvite(interactor, target, kind) => { ServerEvent::InitiateInvite(interactor, target, kind) => {
handle_invite(self, interactor, target, kind) handle_invite(self, interactor, target, kind)

View File

@ -3,7 +3,7 @@
use super::*; use super::*;
use crate::sys::terrain::NpcData; use crate::sys::terrain::NpcData;
use common::{ use common::{
comp::{self, Body, Presence, PresenceKind}, comp::{self, Agent, Body, Presence, PresenceKind},
event::{EventBus, NpcBuilder, ServerEvent}, event::{EventBus, NpcBuilder, ServerEvent},
generation::{BodyBuilder, EntityConfig, EntityInfo}, generation::{BodyBuilder, EntityConfig, EntityInfo},
resources::{DeltaTime, Time, TimeOfDay}, resources::{DeltaTime, Time, TimeOfDay},
@ -138,13 +138,13 @@ fn get_npc_entity_info(npc: &Npc, sites: &Sites, index: IndexRef) -> EntityInfo
let pos = comp::Pos(npc.wpos); let pos = comp::Pos(npc.wpos);
let mut rng = npc.rng(Npc::PERM_ENTITY_CONFIG); let mut rng = npc.rng(Npc::PERM_ENTITY_CONFIG);
if let Some(ref profession) = npc.profession { if let Some(profession) = npc.profession() {
let economy = npc.home.and_then(|home| { let economy = npc.home.and_then(|home| {
let site = sites.get(home)?.world_site?; let site = sites.get(home)?.world_site?;
index.sites.get(site).trade_information(site.id()) index.sites.get(site).trade_information(site.id())
}); });
let config_asset = humanoid_config(profession); let config_asset = humanoid_config(&profession);
let entity_config = EntityConfig::from_asset_expect_owned(config_asset) let entity_config = EntityConfig::from_asset_expect_owned(config_asset)
.with_body(BodyBuilder::Exact(npc.body)); .with_body(BodyBuilder::Exact(npc.body));
@ -156,9 +156,9 @@ fn get_npc_entity_info(npc: &Npc, sites: &Sites, index: IndexRef) -> EntityInfo
comp::Alignment::Npc comp::Alignment::Npc
}) })
.with_economy(economy.as_ref()) .with_economy(economy.as_ref())
.with_lazy_loadout(profession_extra_loadout(npc.profession.as_ref())) .with_lazy_loadout(profession_extra_loadout(Some(&profession)))
.with_alias(npc.get_name()) .with_alias(npc.get_name())
.with_agent_mark(profession_agent_mark(npc.profession.as_ref())) .with_agent_mark(profession_agent_mark(Some(&profession)))
} else { } else {
let config_asset = match npc.body { let config_asset = match npc.body {
Body::BirdLarge(body) => match body.species { Body::BirdLarge(body) => match body.species {
@ -169,14 +169,29 @@ fn get_npc_entity_info(npc: &Npc, sites: &Sites, index: IndexRef) -> EntityInfo
// which limits what species are used // which limits what species are used
_ => unimplemented!(), _ => unimplemented!(),
}, },
_ => unimplemented!(), Body::BipedLarge(body) => match body.species {
comp::biped_large::Species::Ogre => "common.entity.wild.aggressive.ogre",
comp::biped_large::Species::Cyclops => "common.entity.wild.aggressive.cyclops",
comp::biped_large::Species::Wendigo => "common.entity.wild.aggressive.wendigo",
comp::biped_large::Species::Werewolf => "common.entity.wild.aggressive.werewolf",
comp::biped_large::Species::Cavetroll => "common.entity.wild.aggressive.cave_troll",
comp::biped_large::Species::Mountaintroll => {
"common.entity.wild.aggressive.mountain_troll"
},
comp::biped_large::Species::Swamptroll => {
"common.entity.wild.aggressive.swamp_troll"
},
comp::biped_large::Species::Blueoni => "common.entity.wild.aggressive.blue_oni",
comp::biped_large::Species::Redoni => "common.entity.wild.aggressive.red_oni",
comp::biped_large::Species::Tursus => "common.entity.wild.aggressive.tursus",
species => unimplemented!("rtsim spawning for {:?}", species),
},
body => unimplemented!("rtsim spawning for {:?}", body),
}; };
let entity_config = EntityConfig::from_asset_expect_owned(config_asset) let entity_config = EntityConfig::from_asset_expect_owned(config_asset)
.with_body(BodyBuilder::Exact(npc.body)); .with_body(BodyBuilder::Exact(npc.body));
EntityInfo::at(pos.0) EntityInfo::at(pos.0).with_entity_config(entity_config, Some(config_asset), &mut rng)
.with_entity_config(entity_config, Some(config_asset), &mut rng)
.with_alignment(comp::Alignment::Wild)
} }
} }
@ -299,7 +314,10 @@ impl<'a> System<'a> for Sys {
.with_health(health) .with_health(health)
.with_poise(poise) .with_poise(poise)
.with_inventory(inventory) .with_inventory(inventory)
.with_agent(agent) .with_agent(agent.map(|agent| Agent {
rtsim_outbox: Some(Default::default()),
..agent
}))
.with_scale(scale) .with_scale(scale)
.with_loot(loot) .with_loot(loot)
.with_rtsim(RtSimEntity(npc_id)), .with_rtsim(RtSimEntity(npc_id)),
@ -358,7 +376,10 @@ impl<'a> System<'a> for Sys {
.with_health(health) .with_health(health)
.with_poise(poise) .with_poise(poise)
.with_inventory(inventory) .with_inventory(inventory)
.with_agent(agent) .with_agent(agent.map(|agent| Agent {
rtsim_outbox: Some(Default::default()),
..agent
}))
.with_scale(scale) .with_scale(scale)
.with_loot(loot) .with_loot(loot)
.with_rtsim(RtSimEntity(npc_id)), .with_rtsim(RtSimEntity(npc_id)),
@ -402,6 +423,9 @@ impl<'a> System<'a> for Sys {
.rtsim_controller .rtsim_controller
.actions .actions
.extend(std::mem::take(&mut npc.controller.actions)); .extend(std::mem::take(&mut npc.controller.actions));
if let Some(rtsim_outbox) = &mut agent.rtsim_outbox {
npc.inbox.append(rtsim_outbox);
}
} }
}); });
} }

View File

@ -4,6 +4,7 @@ use common::{
AgentEvent, AwarenessState, Target, TimerAction, DEFAULT_INTERACTION_TIME, AgentEvent, AwarenessState, Target, TimerAction, DEFAULT_INTERACTION_TIME,
TRADE_INTERACTION_TIME, TRADE_INTERACTION_TIME,
}, },
dialogue::Subject,
Agent, Alignment, BehaviorCapability, BehaviorState, Body, BuffKind, ControlAction, Agent, Alignment, BehaviorCapability, BehaviorState, Body, BuffKind, ControlAction,
ControlEvent, Controller, InputKind, InventoryEvent, Pos, UtteranceKind, ControlEvent, Controller, InputKind, InventoryEvent, Pos, UtteranceKind,
}, },
@ -494,8 +495,13 @@ fn handle_rtsim_actions(bdata: &mut BehaviorData) -> bool {
.timer .timer
.start(bdata.read_data.time.0, TimerAction::Interact); .start(bdata.read_data.time.0, TimerAction::Interact);
bdata.controller.push_action(ControlAction::Stand); bdata.controller.push_action(ControlAction::Stand);
}
if let Some(target_uid) = bdata.read_data.uids.get(target) {
bdata
.controller
.push_event(ControlEvent::Interact(*target_uid, Subject::Regular));
}
}
bdata.controller.push_utterance(UtteranceKind::Greeting); bdata.controller.push_utterance(UtteranceKind::Greeting);
bdata.agent_data.chat_npc(msg, bdata.event_emitter); bdata.agent_data.chat_npc(msg, bdata.event_emitter);
} }

View File

@ -10,7 +10,7 @@ use common::{
UtteranceKind, UtteranceKind,
}, },
event::ServerEvent, event::ServerEvent,
rtsim::PersonalityTrait, rtsim::{Actor, NpcInput, PersonalityTrait},
trade::{TradeAction, TradePhase, TradeResult}, trade::{TradeAction, TradePhase, TradeResult},
}; };
use rand::{thread_rng, Rng}; use rand::{thread_rng, Rng};
@ -87,8 +87,26 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool {
} }
if let Some(AgentEvent::Talk(by, subject)) = agent.inbox.pop_front() { if let Some(AgentEvent::Talk(by, subject)) = agent.inbox.pop_front() {
let by_entity = get_entity_by_id(by.id(), read_data);
if let Some(rtsim_outbox) = &mut agent.rtsim_outbox {
if let Subject::Regular
| Subject::Mood
| Subject::Work = subject
&& let Some(by_entity) = by_entity
&& let Some(actor) = read_data.presences
.get(by_entity)
.and_then(|p| p.kind.character_id().map(Actor::Character))
.or_else(|| Some(Actor::Npc(read_data.rtsim_entities
.get(by_entity)?.0)))
{
rtsim_outbox.push_back(NpcInput::Interaction(actor, subject));
return false;
}
}
if agent.allowed_to_speak() { if agent.allowed_to_speak() {
if let Some(target) = get_entity_by_id(by.id(), read_data) { if let Some(target) = by_entity {
let target_pos = read_data.positions.get(target).map(|pos| pos.0); let target_pos = read_data.positions.get(target).map(|pos| pos.0);
agent.target = Some(Target::new( agent.target = Some(Target::new(
@ -216,15 +234,18 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool {
if let Some(src_pos) = read_data.positions.get(target) { if let Some(src_pos) = read_data.positions.get(target) {
// TODO: Localise // TODO: Localise
let msg = if let Some(person_pos) = person.origin { let msg = if let Some(person_pos) = person.origin {
let distance = Distance::from_dir(person_pos.xy() - src_pos.0.xy()); let distance =
Distance::from_dir(person_pos.xy().as_() - src_pos.0.xy());
match distance { match distance {
Distance::NextTo | Distance::Near => { Distance::NextTo | Distance::Near => {
format!( format!(
"{} ? I think he's {} {} from here!", "{} ? I think he's {} {} from here!",
person.name(), person.name(),
distance.name(), distance.name(),
Direction::from_dir(person_pos.xy() - src_pos.0.xy(),) Direction::from_dir(
.name() person_pos.xy().as_() - src_pos.0.xy()
)
.name()
) )
}, },
_ => { _ => {

View File

@ -15,6 +15,7 @@ use client::{self, Client};
use common::{ use common::{
comp, comp,
comp::{ comp::{
dialogue::Subject,
inventory::slot::{EquipSlot, Slot}, inventory::slot::{EquipSlot, Slot},
invite::InviteKind, invite::InviteKind,
item::{tool::ToolKind, ItemDesc}, item::{tool::ToolKind, ItemDesc},
@ -959,7 +960,7 @@ impl PlayState for SessionState {
// TODO: maybe start crafting instead? // TODO: maybe start crafting instead?
client.toggle_sit(); client.toggle_sit();
} else { } else {
client.npc_interact(*entity); client.npc_interact(*entity, Subject::Regular);
} }
}, },
} }