diff --git a/common/src/rtsim.rs b/common/src/rtsim.rs index eb5913c1aa..887cfadab9 100644 --- a/common/src/rtsim.rs +++ b/common/src/rtsim.rs @@ -7,7 +7,7 @@ use crate::character::CharacterId; use rand::{seq::IteratorRandom, Rng}; use serde::{Deserialize, Serialize}; use specs::Component; -use std::collections::VecDeque; +use std::{borrow::Cow, collections::VecDeque}; use strum::{EnumIter, IntoEnumIterator}; use vek::*; @@ -219,9 +219,11 @@ pub enum NpcActivity { Dance, } -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Debug)] pub enum NpcAction { Greet(Actor), + // TODO: Use some sort of structured, language-independent value that frontends can translate instead + Say(Cow<'static, str>), } #[derive(Copy, Clone, Debug, Serialize, Deserialize, enum_map::Enum)] diff --git a/rtsim/src/data/npc.rs b/rtsim/src/data/npc.rs index 880e989e46..4ff70046f8 100644 --- a/rtsim/src/data/npc.rs +++ b/rtsim/src/data/npc.rs @@ -15,6 +15,7 @@ use rand::prelude::*; use serde::{Deserialize, Serialize}; use slotmap::HopSlotMap; use std::{ + borrow::Cow, collections::VecDeque, ops::{Deref, DerefMut}, }; @@ -69,6 +70,10 @@ impl Controller { pub fn do_dance(&mut self) { self.activity = Some(NpcActivity::Dance); } pub fn greet(&mut self, actor: Actor) { self.actions.push(NpcAction::Greet(actor)); } + + pub fn say(&mut self, msg: impl Into>) { + self.actions.push(NpcAction::Say(msg.into())); + } } pub struct Brain { diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index e59d4aeb97..86bc1c6218 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -348,7 +348,7 @@ where /// Try to travel to a site. Where practical, paths will be taken. fn travel_to_point(wpos: Vec2) -> impl Action { now(move |ctx| { - const WAYPOINT: f32 = 24.0; + const WAYPOINT: f32 = 48.0; let start = ctx.npc.wpos.xy(); let diff = wpos - start; let n = (diff.magnitude() / WAYPOINT).max(1.0); @@ -506,9 +506,12 @@ fn adventure() -> impl Action { } else { 60.0 * 3.0 }; + let site_name = ctx.state.data().sites[tgt_site].world_site + .map(|ws| ctx.index.sites.get(ws).name().to_string()) + .unwrap_or_default(); // Travel to the site - important( - travel_to_site(tgt_site) + important(just(move |ctx| ctx.controller.say(format!("I've spent enough time here, onward to {}!", site_name))) + .then(travel_to_site(tgt_site)) // Stop for a few minutes .then(villager(tgt_site).repeat().stop_if(timeout(wait_time))) .map(|_| ()) @@ -597,10 +600,12 @@ fn villager(visiting_site: SiteId) -> impl Action { Some(site2.tile_center_wpos(house.root_tile()).as_()) }) { - travel_to_point(house_wpos) + just(|ctx| ctx.controller.say("It's dark, time to go home")) + .then(travel_to_point(house_wpos)) .debug(|| "walk to house") .then(socialize().repeat().debug(|| "wait in house")) .stop_if(|ctx| DayPeriod::from(ctx.time_of_day.0).is_light()) + .then(just(|ctx| ctx.controller.say("A new day begins!"))) .map(|_| ()) .boxed() } else { @@ -610,9 +615,11 @@ fn villager(visiting_site: SiteId) -> impl Action { .debug(|| "find somewhere to sleep"), ); // Villagers with roles should perform those roles - } else if matches!(ctx.npc.profession, Some(Profession::Herbalist)) { + } else if matches!(ctx.npc.profession, Some(Profession::Herbalist)) + && thread_rng().gen_bool(0.8) + { if let Some(forest_wpos) = find_forest(ctx) { - return important( + return casual( travel_to_point(forest_wpos) .debug(|| "walk to forest") .then({ @@ -622,10 +629,13 @@ fn villager(visiting_site: SiteId) -> impl Action { .map(|_| ()), ); } - } else if matches!(ctx.npc.profession, Some(Profession::Hunter)) { + } else if matches!(ctx.npc.profession, Some(Profession::Hunter)) + && thread_rng().gen_bool(0.8) + { if let Some(forest_wpos) = find_forest(ctx) { - return important( - travel_to_point(forest_wpos) + return casual( + just(|ctx| ctx.controller.say("Time to go hunting!")) + .then(travel_to_point(forest_wpos)) .debug(|| "walk to forest") .then({ let wait_time = thread_rng().gen_range(30.0..60.0); diff --git a/rtsim/src/rule/simulate_npcs.rs b/rtsim/src/rule/simulate_npcs.rs index f59f1cb037..81140b6a8a 100644 --- a/rtsim/src/rule/simulate_npcs.rs +++ b/rtsim/src/rule/simulate_npcs.rs @@ -292,7 +292,7 @@ impl Rule for SimulateNpcs { // Consume NPC actions for action in std::mem::take(&mut npc.controller.actions) { match action { - NpcAction::Greet(_) => {}, // Currently, just swallow greeting actions + NpcAction::Greet(_) | NpcAction::Say(_) => {}, // Currently, just swallow interactions } } diff --git a/server/src/sys/agent/behavior_tree.rs b/server/src/sys/agent/behavior_tree.rs index d76f24156a..e1a51b40f0 100644 --- a/server/src/sys/agent/behavior_tree.rs +++ b/server/src/sys/agent/behavior_tree.rs @@ -505,6 +505,9 @@ fn handle_rtsim_actions(bdata: &mut BehaviorData) -> bool { } } }, + NpcAction::Say(msg) => { + bdata.agent_data.chat_npc(msg, &mut bdata.event_emitter); + }, } } false