diff --git a/client/src/lib.rs b/client/src/lib.rs index d52ea4292e..af7ca5e7cb 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -23,6 +23,7 @@ use common::{ self, chat::KillSource, controller::CraftEvent, + dialogue::Subject, group, inventory::item::{modular, tool, ItemKind}, 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 self.is_dead() { return; } 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, + ))); } } diff --git a/common/src/comp/agent.rs b/common/src/comp/agent.rs index cb34b91852..39f9d23205 100644 --- a/common/src/comp/agent.rs +++ b/common/src/comp/agent.rs @@ -4,7 +4,7 @@ use crate::{ quadruped_small, ship, Body, UtteranceKind, }, path::Chaser, - rtsim::RtSimController, + rtsim::{NpcInput, RtSimController}, trade::{PendingTrade, ReducedInventory, SiteId, SitePrices, TradeId, TradeResult}, uid::Uid, }; @@ -569,6 +569,8 @@ pub struct Agent { /// required and reset each time the flee timer is reset. pub flee_from_pos: Option, pub awareness: Awareness, + /// Inputs sent up to rtsim + pub rtsim_outbox: Option>, } #[derive(Clone, Debug)] @@ -673,6 +675,7 @@ impl Agent { position_pid_controller: None, flee_from_pos: None, awareness: Awareness::new(0.0), + rtsim_outbox: None, } } diff --git a/common/src/comp/controller.rs b/common/src/comp/controller.rs index bdde986394..6689c3f781 100644 --- a/common/src/comp/controller.rs +++ b/common/src/comp/controller.rs @@ -1,6 +1,7 @@ use crate::{ comp::{ ability, + dialogue::Subject, inventory::{ item::tool::ToolKind, slot::{EquipSlot, InvSlotId, Slot}, @@ -137,7 +138,7 @@ pub enum ControlEvent { //ToggleLantern, EnableLantern, DisableLantern, - Interact(Uid), + Interact(Uid, Subject), InitiateInvite(Uid, InviteKind), InviteResponse(InviteResponse), PerformTradeAction(TradeId, TradeAction), diff --git a/common/src/comp/dialogue.rs b/common/src/comp/dialogue.rs index 6149563dd3..9ef5b833ae 100644 --- a/common/src/comp/dialogue.rs +++ b/common/src/comp/dialogue.rs @@ -1,23 +1,24 @@ +use serde::{Deserialize, Serialize}; use vek::{Vec2, Vec3}; use super::Item; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct AskedLocation { pub name: String, pub origin: Vec2, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum PersonType { Merchant, Villager { name: String }, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct AskedPerson { pub person_type: PersonType, - pub origin: Option>, + pub origin: Option>, } impl AskedPerson { @@ -30,7 +31,7 @@ impl AskedPerson { } /// Conversation subject -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum Subject { /// Using simple interaction with NPC /// This is meant to be the default behavior of talking diff --git a/common/src/comp/presence.rs b/common/src/comp/presence.rs index fcb7588993..b63d4a6165 100644 --- a/common/src/comp/presence.rs +++ b/common/src/comp/presence.rs @@ -40,6 +40,14 @@ impl PresenceKind { /// certain in-game messages from the client such as control inputs /// should be handled. pub fn controlling_char(&self) -> bool { matches!(self, Self::Character(_) | Self::Possessor) } + + pub fn character_id(&self) -> Option { + if let Self::Character(character_id) = self { + Some(*character_id) + } else { + None + } + } } #[derive(PartialEq, Debug, Clone, Copy)] diff --git a/common/src/event.rs b/common/src/event.rs index 6f5b2f15be..2cdfed9c49 100644 --- a/common/src/event.rs +++ b/common/src/event.rs @@ -3,6 +3,7 @@ use crate::{ comp::{ self, agent::Sound, + dialogue::Subject, invite::{InviteKind, InviteResponse}, DisconnectReason, Ori, Pos, }, @@ -189,7 +190,7 @@ pub enum ServerEvent { }, EnableLantern(EcsEntity), DisableLantern(EcsEntity), - NpcInteract(EcsEntity, EcsEntity), + NpcInteract(EcsEntity, EcsEntity, Subject), InviteResponse(EcsEntity, InviteResponse), InitiateInvite(EcsEntity, Uid, InviteKind), ProcessTradeAction(EcsEntity, TradeId, TradeAction), diff --git a/common/src/rtsim.rs b/common/src/rtsim.rs index 6b737cd7e0..9617aa7109 100644 --- a/common/src/rtsim.rs +++ b/common/src/rtsim.rs @@ -3,7 +3,10 @@ // `Agent`). When possible, this should be moved to the `rtsim` // module in `server`. -use crate::{character::CharacterId, comp::Content}; +use crate::{ + character::CharacterId, + comp::{dialogue::Subject, Content}, +}; use rand::{seq::IteratorRandom, Rng}; use serde::{Deserialize, Serialize}; 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 ReportId; } + #[derive(Copy, Clone, Debug)] pub struct RtSimEntity(pub NpcId); @@ -258,6 +263,13 @@ pub enum NpcAction { 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 // identifiers for the sake of rtsim persistence #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, enum_map::Enum)] diff --git a/common/systems/src/controller.rs b/common/systems/src/controller.rs index 0102b5ee11..46ce5973e2 100644 --- a/common/systems/src/controller.rs +++ b/common/systems/src/controller.rs @@ -66,12 +66,13 @@ impl<'a> System<'a> for Sys { ControlEvent::DisableLantern => { server_emitter.emit(ServerEvent::DisableLantern(entity)) }, - ControlEvent::Interact(npc_uid) => { + ControlEvent::Interact(npc_uid, subject) => { if let Some(npc_entity) = read_data .uid_allocator .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) => { diff --git a/rtsim/src/ai/mod.rs b/rtsim/src/ai/mod.rs index dfaf40f07c..1bcc653451 100644 --- a/rtsim/src/ai/mod.rs +++ b/rtsim/src/ai/mod.rs @@ -5,7 +5,10 @@ use crate::{ }, RtState, }; -use common::resources::{Time, TimeOfDay}; +use common::{ + resources::{Time, TimeOfDay}, + rtsim::NpcInput, +}; use hashbrown::HashSet; use itertools::Either; use rand_chacha::ChaChaRng; @@ -26,7 +29,7 @@ pub struct NpcCtx<'a> { pub npc_id: NpcId, pub npc: &'a Npc, pub controller: &'a mut Controller, - pub inbox: &'a mut VecDeque, // TODO: Allow more inbox items + pub inbox: &'a mut VecDeque, // TODO: Allow more inbox items pub sentiments: &'a mut Sentiments, pub known_reports: &'a mut HashSet, diff --git a/rtsim/src/data/npc.rs b/rtsim/src/data/npc.rs index 26cb86c379..4294d2bebd 100644 --- a/rtsim/src/data/npc.rs +++ b/rtsim/src/data/npc.rs @@ -1,6 +1,6 @@ use crate::{ ai::Action, - data::{ReportId, Reports, Sentiments}, + data::{Reports, Sentiments}, gen::name, }; pub use common::rtsim::{NpcId, Profession}; @@ -9,8 +9,8 @@ use common::{ comp, grid::Grid, rtsim::{ - Actor, ChunkResource, FactionId, NpcAction, NpcActivity, Personality, Role, SiteId, - VehicleId, + Actor, ChunkResource, FactionId, NpcAction, NpcActivity, NpcInput, Personality, ReportId, + Role, SiteId, VehicleId, }, store::Id, terrain::CoordinateConversions, @@ -121,7 +121,7 @@ pub struct Npc { #[serde(skip)] pub controller: Controller, #[serde(skip)] - pub inbox: VecDeque, + pub inbox: VecDeque, /// 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 diff --git a/rtsim/src/data/report.rs b/rtsim/src/data/report.rs index 127c0c6653..2b841a0aa4 100644 --- a/rtsim/src/data/report.rs +++ b/rtsim/src/data/report.rs @@ -4,7 +4,7 @@ use slotmap::HopSlotMap; use std::ops::Deref; 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. /// diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index 85c86186a1..2bfba3a1a8 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -13,10 +13,11 @@ use common::{ astar::{Astar, PathResult}, comp::{ compass::{Direction, Distance}, + dialogue::Subject, Content, }, path::Path, - rtsim::{Actor, ChunkResource, Profession, Role, SiteId}, + rtsim::{Actor, ChunkResource, NpcInput, Profession, Role, SiteId}, spiral::Spiral2d, store::Id, terrain::{CoordinateConversions, SiteKindMeta, TerrainChunkSize}, @@ -542,6 +543,41 @@ 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) } +fn talk_to(tgt: Actor, subject: Option) -> impl Action { + now(move |ctx| { + // 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) + }; + just(move |ctx| ctx.controller.say(tgt, comment.clone())) + }) +} + fn socialize() -> impl Action { now(|ctx| { // Skip most socialising actions if we're not loaded @@ -563,36 +599,7 @@ fn socialize() -> impl Action { .nearby(Some(ctx.npc_id), ctx.npc.wpos, 8.0) .choose(&mut ctx.rng) { - // 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) - }; - return just(move |ctx| ctx.controller.say(other, comment.clone())) + return talk_to(other, None) // After talking, wait for a while .then(idle().repeat().stop_if(timeout(4.0))) .map(|_| ()) @@ -972,7 +979,7 @@ fn captain() -> impl Action { fn check_inbox(ctx: &mut NpcCtx) -> Option { loop { 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)] match ctx.state.data().reports.get(report_id).map(|r| r.kind) { Some(ReportKind::Death { killer, actor, .. }) @@ -1013,15 +1020,17 @@ fn check_inbox(ctx: &mut NpcCtx) -> Option { "npc-speech-witness_death" }; ctx.known_reports.insert(report_id); - break Some(just(move |ctx| { - ctx.controller.say(killer, Content::localized(phrase)) - })); + break Some( + just(move |ctx| ctx.controller.say(killer, Content::localized(phrase))) + .l(), + ); }, 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, } } diff --git a/rtsim/src/rule/report.rs b/rtsim/src/rule/report.rs index 74874a2605..7b3ff94d7c 100644 --- a/rtsim/src/rule/report.rs +++ b/rtsim/src/rule/report.rs @@ -3,6 +3,7 @@ use crate::{ event::{EventCtx, OnDeath}, RtState, Rule, RuleError, }; +use common::rtsim::NpcInput; pub struct ReportEvents; @@ -38,7 +39,7 @@ fn on_death(ctx: EventCtx) { // data structure in their own time. for npc_id in nearby { if let Some(npc) = data.npcs.get_mut(npc_id) { - npc.inbox.push_back(report); + npc.inbox.push_back(NpcInput::Report(report)); } } } diff --git a/rtsim/src/rule/sync_npcs.rs b/rtsim/src/rule/sync_npcs.rs index d89ffc3416..90f7dac625 100644 --- a/rtsim/src/rule/sync_npcs.rs +++ b/rtsim/src/rule/sync_npcs.rs @@ -2,7 +2,11 @@ use crate::{ event::{EventCtx, OnDeath, OnSetup, OnTick}, RtState, Rule, RuleError, }; -use common::{grid::Grid, rtsim::Actor, terrain::CoordinateConversions}; +use common::{ + grid::Grid, + rtsim::{Actor, NpcInput}, + terrain::CoordinateConversions, +}; pub struct SyncNpcs; @@ -124,7 +128,8 @@ fn on_tick(ctx: EventCtx) { npc.inbox.extend(site.known_reports .iter() .copied() - .filter(|report| !npc.known_reports.contains(report))); + .filter(|report| !npc.known_reports.contains(report)) + .map(NpcInput::Report)); } } diff --git a/server/src/events/interaction.rs b/server/src/events/interaction.rs index 0345d72c7e..6b636008b4 100644 --- a/server/src/events/interaction.rs +++ b/server/src/events/interaction.rs @@ -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(); if let Some(agent) = state .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) { agent .inbox - .push_back(AgentEvent::Talk(interactor_uid, Subject::Regular)); + .push_back(AgentEvent::Talk(interactor_uid, subject)); } } } diff --git a/server/src/events/mod.rs b/server/src/events/mod.rs index 6e2a28e3d7..6524da88f2 100644 --- a/server/src/events/mod.rs +++ b/server/src/events/mod.rs @@ -123,8 +123,8 @@ impl Server { }, ServerEvent::EnableLantern(entity) => handle_lantern(self, entity, true), ServerEvent::DisableLantern(entity) => handle_lantern(self, entity, false), - ServerEvent::NpcInteract(interactor, target) => { - handle_npc_interaction(self, interactor, target) + ServerEvent::NpcInteract(interactor, target, subject) => { + handle_npc_interaction(self, interactor, target, subject) }, ServerEvent::InitiateInvite(interactor, target, kind) => { handle_invite(self, interactor, target, kind) diff --git a/server/src/rtsim/tick.rs b/server/src/rtsim/tick.rs index 5a2bab3fab..c7793c996e 100644 --- a/server/src/rtsim/tick.rs +++ b/server/src/rtsim/tick.rs @@ -3,7 +3,7 @@ use super::*; use crate::sys::terrain::NpcData; use common::{ - comp::{self, Body, Presence, PresenceKind}, + comp::{self, Agent, Body, Presence, PresenceKind}, event::{EventBus, NpcBuilder, ServerEvent}, generation::{BodyBuilder, EntityConfig, EntityInfo}, resources::{DeltaTime, Time, TimeOfDay}, @@ -314,7 +314,10 @@ impl<'a> System<'a> for Sys { .with_health(health) .with_poise(poise) .with_inventory(inventory) - .with_agent(agent) + .with_agent(agent.map(|agent| Agent { + rtsim_outbox: Some(Default::default()), + ..agent + })) .with_scale(scale) .with_loot(loot) .with_rtsim(RtSimEntity(npc_id)), @@ -373,7 +376,10 @@ impl<'a> System<'a> for Sys { .with_health(health) .with_poise(poise) .with_inventory(inventory) - .with_agent(agent) + .with_agent(agent.map(|agent| Agent { + rtsim_outbox: Some(Default::default()), + ..agent + })) .with_scale(scale) .with_loot(loot) .with_rtsim(RtSimEntity(npc_id)), @@ -417,6 +423,9 @@ impl<'a> System<'a> for Sys { .rtsim_controller .actions .extend(std::mem::take(&mut npc.controller.actions)); + if let Some(rtsim_outbox) = &mut agent.rtsim_outbox { + npc.inbox.append(rtsim_outbox); + } } }); } diff --git a/server/src/sys/agent/behavior_tree/interaction.rs b/server/src/sys/agent/behavior_tree/interaction.rs index d9f3e23bad..6e3f093782 100644 --- a/server/src/sys/agent/behavior_tree/interaction.rs +++ b/server/src/sys/agent/behavior_tree/interaction.rs @@ -10,7 +10,7 @@ use common::{ UtteranceKind, }, event::ServerEvent, - rtsim::PersonalityTrait, + rtsim::{Actor, NpcInput, PersonalityTrait}, trade::{TradeAction, TradePhase, TradeResult}, }; 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() { + 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 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); 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) { // TODO: Localise 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 { Distance::NextTo | Distance::Near => { format!( "{} ? I think he's {} {} from here!", person.name(), distance.name(), - Direction::from_dir(person_pos.xy() - src_pos.0.xy(),) - .name() + Direction::from_dir( + person_pos.xy().as_() - src_pos.0.xy() + ) + .name() ) }, _ => { diff --git a/voxygen/src/session/mod.rs b/voxygen/src/session/mod.rs index dab0b8e508..e47fb0d43d 100644 --- a/voxygen/src/session/mod.rs +++ b/voxygen/src/session/mod.rs @@ -15,6 +15,7 @@ use client::{self, Client}; use common::{ comp, comp::{ + dialogue::Subject, inventory::slot::{EquipSlot, Slot}, invite::InviteKind, item::{tool::ToolKind, ItemDesc}, @@ -959,7 +960,7 @@ impl PlayState for SessionState { // TODO: maybe start crafting instead? client.toggle_sit(); } else { - client.npc_interact(*entity); + client.npc_interact(*entity, Subject::Regular); } }, }